Toren in Python: Iteratoren, Generatoren, Dekoratoren

Toren in Python: Iteratoren, Generatoren, Dekoratoren

Reinhard Wobst, r dot wobst at gmx dot de

@(#) Jan 06 2005, 17:46:54

0. Zur Erinnerung

Beispiel (Decodieren von quoted Printäppel):

#!/usr/bin/env python

import sys
from binascii import a2b_hex

lns = sys.stdin.read()          # file as one string
lns = lns.replace('=\n', '')    # join marked lines
L = lns.split('=')              # bring hex numbers at begin of elements 1,2,...
ll = (map(lambda x: '%s%s' % (a2b_hex(x[:2]), x[2:]), L[1:]))
                                # replace hex numbers by characters
print ''.join([L[0]]+ll)

1. Iteratoren und Generatoren (ab Python2.3)

Iteratoren

L = [1,3,4,5,7]
for i in L:
    print i

Äquivalent zu:

L = [1,3,4,5,7]
Li = iter(L)

for i in Li:
    print i

Die Schleife ist zu verstehen als:


while 1:
    try:
        i = Li.next()
        print i
    except StopIteration:
        break

Li ist ein Iterator, entsprechend einer Klasse, die Methoden __iter__() und next() unterstützt (Details vgl. Library Reference, 2.3.5). Konzept von C++/STL her bekannt!

Anwendung:

Klassisch:

for line in fd.readlines():
    print line
(alle Zeilen im RAM!) oder
while 1:
    line = fd.readline()
    if not line: break
    print line

Besseres Beispiel:

for i in range(10000000):
...
hält 10 Mio. Listenelemente a 16 Byte im Speicher; deswegen schreib man schon immer
for i in xrange(10000000):
...
xrange ist ein Iterator, eine "Element-Fabrik". Ist verallgemeinerbar:

Man kann auch Klassen mit entspr. Methoden versehen, die sich dann wie Container verhalten - vgl. Language Reference (Reference Manual), 3.3.5

Generatoren

Funktionen, die das yield - Statement enthalten, heißen Generatorfunktionen. Sie geben einen Iterator zurück, keinen Wert! So kann man xrange() selbst definieren:

def Xrange(N):
    n = 0
    while n < N:
        yield n		# return
        n += 1		# nächster Einsprungpunkt

for i in Xrange(7):
    print i

'yield' ist ein "return, das den Zustand einfriert". Xrange() ist keine übliche Funktion mehr, sondern gibt eine Klasseninstanz mit next()-Methode zurück (ist aber nicht einfach als Klasse zu implementieren!):

NN = Xrange(7)		# Initialisierung
for i in NN:
...

Weiteres Beispiel: Verarbeiten der Worte eines Riesenfiles (notwendig als Stream):

import sys

def read_word(filename):
    for line in open(filename, 'r'):
        for word in line.split():
            yield word

for word in read_word(sys.argv[1]):
    print word

Klassisch als Klasse, zum Vergleich:

class read_word:
    def __init__(self, filename):
        self.fd = open(filename, 'r')
        self.indx = self.lng = 0

    def retword(self):
        while self.indx >= self.lng:
            line = self.fd.readline()
            if not line:
                return None
            self.linesp = line.split()
            self.lng = len(self.linesp)-1
            if self.lng < 0: continue
            self.indx = -1
            break
        self.indx += 1
        return self.linesp[self.indx]


rw = read_word(sys.argv[1])
while 1:
    word = rw.retword()
    if not word: break
    print word

Nachteil: Kein Destruktor bei Generatorfunktionen, im Beispiel: File wird nicht automatisch geschlossen. (Ausweg: in Klasse read_word eine close()-Methode definieren, oder __del__() definieren).

Bindung von Ressourcen an Lebenszeit von Objekten nur in C++, nicht in einer Skriptsprache mit Garbage Collector! (Problem bei Java - Python kennt wenigstens Destruktoren, die allerdings asynchron gerufen werden).

Generator Expressions (Python2.4)

Kurzform von Generatorfunktionen, ähnlich zu list comprehensions:

(x*x for x in L)

Einzelheiten in Reference Manual, 5.2.5.

Beispiel:

ends = {}

for sq in (x*x for x in xrange(1000000)):
    digits = '06%d' % sq
    last6 = digits[-6:]
    ends[last6] = 1	# irgendein Wert (hier 1)

print '%d verschiedene letzte 6 Ziffern von Quadratzahlen' % len(ends.keys())

(Rechenzeit Athlon 1700: 6sec; 78184 verschiedene Endungen)

Vorteil gegenüber list comprehension: Speicherplatz gespart:

for sq in [x*x for x in L]
berechnet zunächst gesamte Liste, dann erst wird "for sq in [...]" ausgewertet.

2. Neues in Python2.4

Mehr dazu in "What's new in Python2.4?", 8. (Online-Doku)

3. Dekoratoren (Python2.4)

Ungewohnte Syntax (in Anlehnung an Java):

@decorfct
def fct(arg):
   ...

ist eine Art Wrapper um fct(): Es wird in Wirklichkeit mit decorfct(fct) gerechnet. decorfct hat als Argument ein callable und muss ein solches zurückgeben.

Sinnvolles Beispiel (Überprüfung der Argumente auf ganzzahligen Typ):

def require_int(func):
    def wrapper(arg):
        assert isinstance(arg, int)
        return func(arg)

    return wrapper

@require_int
def p1(arg):
    print arg

Näheres dazu in der Online-Doku "What's New in Python?", 5., und im folgenden Punkt.

4. Klassen etwas moderner (Python2.2/2.4)

staticmethod

Methoden von Python-Klassen haben standardmäßig die Klasseninstanz als erstes Element:

class SvenDietmar:
  def __init__(self, name):
    self.name = name
  def invite(self, month):
    print 'Der Stammtisch im Monat %s' % month
    ...
    print 3*'\t', self.name
  ...

(self = "this" in C++).

Aufruf: D2 = SvenDietmar("Dietmar"); D2.invite('Januar')

Analog zu in C++ gibt es statische Funktionen, die an die Klasse, nicht an die Instanz gebunden sind (z.B. zur Initialisierung klassengebundener Daten):

class SvenDietmar:
  def __init_stammtisch():
    SvenDietmar.phrase = "findet wie üblich am"
    ...
  init_stammtisch = staticmethod(__init_stammtisch)	# !

(staticmethod() fügt das Argument in die Methodenliste der Klasse ein.)

Aufruf: SvenDietmar.init_stammtisch()

Bevorzugte Schreibweise seit Python2.4:

class SvenDietmar:
  @staticmethod
  def init_stammtisch():
    SvenDietmar.phrase = "findet wie üblich am"
    ...

Konkretes Beispiel:

class B:
    @staticmethod
    def init(arg):
        B.arg = arg
    def prt(self):
        print B.arg

>>> B.init('holla')
>>> A = B()
>>> A.prt()
holla

classmethod

Weiter gibt es class methods, ebenfalls instanz-unabhängig definiert, aber klassenabhängig. Beispiel:

class A:
    name = 'A'		# klassengebundene Variable
    @classmethod
    def init(cls,arg):		# 1. Argument ist Klasse, nicht Instanz!
        cls.arg = '%s aus Klasse %s' % (arg, cls.name)
    def prt(self):
        print A.arg

class B(A):             # vererbt
    name = 'B'		# klassengebundene Variable
    def prt(self):
        print B.arg

>>> A.init('huhu')
>>> B.init('holla')
>>> C = A(); D = B()
>>> C.prt(); D.prt()
huhu aus Klasse A
holla aus Klasse B

Eine direkte Entsprechung in C++ gibt es nicht!

new classes (ab Python2.2)

Nachteil: Von in C implementierten Klassen konnte man bisher nicht erben.

Beispiel:

class A(int):
    def __init__(self, n):
        self.val = n
    def __lshift__(self, s):
        return ((self.val << s) & 0xffffffff) | (self.val >> (32-s))

x = A(1<<30)
print 'x: %x' % x
print 'x-1: %x' % (x-1)
print 'x >> 2: %x' % (x >> 2)
print 'x << 2: %x' % (x << 2)

Ausgabe:
x: 40000000
x-1: 3fffffff
x >> 2: 10000000
x << 2: 1

Sinnvolleres Beispiel (aus der Doku):

class LockableFile(file):
    def lock (self, operation, length=0, start=0, whence=0):
        import fcntl
        return fcntl.lockf(self.fileno(), operation,
                           length, start, whence)

Da von der neuen Funktion file() geerbt, kann man nun schreiben

fd = LockableFile('carsten.grm', 'w')
fd.lock(...)

Das Modul posixfile wird dadurch überflüssig.

Einige Bemerkungen dazu:

Näheres dazu: Dokumentation zu Python2.2, "What's new in Python2.2" (online auf www.python.org). Dokumentation hier noch lückenhaft, diese Bemerkungen hier dienen nur als Orientierungshilfe!

5. Schlussbemerkungen

5.1 Weitere Entwicklungsrichtung - Codestyle

>>> import this

Jedem empfohlen! (Zen of Python)

list comprehensions

[expr for varname in list]
[expr for varname in list if cond]

Sollen nach PEP3000 map() und filter() ersetzen, sind aber nicht immer so einprägsam:

>>> def f(x): return x*x
>>> map(f, [1,2,3,4])
[1, 4, 9, 16]

5.2 Fehlerbehandlung

EAFP = easier ask for forgiveness than permission: try - catch
(auch selbst definierte Ausnahmeklassen häufig!)
LBYL = look before you leap, viele if-statements (und doch noch etwas vergessen :-)

Eindeutig EAFP vorzuziehen, wie in moderneren Sprachen (seit C++).

5.3 Vorsicht!

Komplexität:

Ressourcenverbrauch:

5.3 Gelöste Probleme 2004 - Beispiel für Produktivität von Python

Zusätzlich für AMD noch 1500 Zeilen unter NDA :-) (das Meiste "nebenbei"; April/Mai z.B. noch 5500 Zeilen C++)