Design Patterns in Python

Abstrakte Methoden und Klassen

Python kennt keine abstrakte Methoden:
  • es könne keine Interfaceklassen mit klassischen Python Sprachmitteln implementiert werden, hingegen mit Metaklassen: Metaclass for Interface Checking
  • das wesentlich Strukturmittel der Design Patterns ist es aber, gegen das Interface zu programmierenZ

Zwei Exceptionstypen werden gerne verwendet, um abstrakte Methoden/Klassen zu simulieren

  1. assert 0, "method is abstract"
  2. raise NotImplementedError
  • insbesonder der erste Ansatz ist mit Vorsicht zu genießen, den ein Aufruf des python Interpreters mit -O optimiert die Assertion weg

Abstrakte Methode

class Interface:
def showMe(self):
raise NotImplementedError

class Implementation( Interface ):
def showMe(self):
print "implemented"

imp=Implementation()
int=Interface()
imp.showMe()

int.showMe()
  • ergibt:
    implemented
    Traceback (most recent call last):
    File "abstractMethode.py", line 12, in ?
    int.showMe()
    File "abstractMethode.py", line 3, in showMe
    raise NotImplementedError
    NotImplementedError

Abstrake Klasse

class Interface:
def __init__(self):
print "init Interface"
raise NotImplementedError
def showMe(self):
print "Interface"

class Implementation( Interface ) :
def __init__(self):
print "init Implementation"
Interface.showMe(self)


imp=Implementation()

int=Interface()
  • ergibt:
    init Implementation
    Interface
    init Interface
    Traceback (most recent call last):
    File "abstractClass.py", line 11, in ?
    int=Interface()
    File "abstractClass.py", line 4, in __init__

    raise NotImplementedError
    NotImplementedError

Dynamische Typisierung

Template Methode

Kann mit Python coffeeinhaltiges Getränk gekocht werden?
  • Wie sieht nun der Java Code mit Python aus?
  • Interface Coffeein:
class Coffeein:

def anleitung(self):
self.wasserKochen()
self.coffeeinHinzufuegen()
self.wasserAufgiessen()
self.zutatHinzufuegen()
self.ruehreUm()

def wasserKochen(self):
print "Koche das Wasser"

def wasserAufgiessen(self):
print "Giesse das Wasser auf"

def ruehreUm(self):
print "Rühre um"

def coffeeinHinzufuegen(self):
raise NotImplementedError

def zutatHinzufuegen(self):
raise NotImplementedError
  • Implementierung Kaffee
import Coffeein

class Kaffee(Coffeein.Coffeein):

def coffeeinHinzufuegen(self):
print "Gib das Kaffeepulver in die Tasse"

def zutatHinzufuegen(self):
print "Füge den Zucker hinzu"
  • Implementierung Tee
import Coffeein

class Tee(Coffeein.Coffeein):

def coffeeinHinzufuegen(self):
print "Gib den Teebeutel in die Tasse"

def zutatHinzufuegen(self):
print "Gib die Milch hinzu"
  • Anleitung
import Coffeein
import Kaffee

import Tee

if __name__ == "__main__":
kaffee = Kaffee.Kaffee()

tee = Tee.Tee()
kaffee.anleitung()
print "----------------------"

tee.anleitung()
  • ergibt
    Koche das Wasser
    Gib das Kaffeepulver in die Tasse
    Giesse das Wasser auf
    Füge den Zucker hinzu
    Rühre um
    ----------------------
    Koche das Wasser
    Gib dem Teebeutel in die Tasse
    Giesse das Wasser auf
    Gib die Milch hinzu
    Rühre um

clone Protokoll

import copy 

class Cloner(object):
def clone(self):
return copy.deepcopy(self)
  • durch das Ableiten der Basisklasse Cloner von object ist Cloner eine new-style class
  • copy.deepcopy löst die neue Instanz von allen Referenzen zu self

Fabrikmethode old-style class

klassische - statisch

Häufig trifft gibt es Sourcecode der Form.
const Window* getNewWindow( const& Window oldWindow){

int type= oldWindow.getType();

Window* newWindow;
switch( type ){
case 0:{
newWindow= new NullWindow;
break;
}
case 1:{
newWindow= new EinsWindow;
break;
}
case 2:{
newWindow= new ZweiWindow;
break;
}
case 3:{
newWindow= new DreiWindow;
break;
}

...
default:{
newWindow= new DefaultWindow;

}

return newWindow;
Dies lässt sich in Java/C++ dadurch lösen, daß die Basisklasse eine virutelle Fabrikmethode clone() erhält, die in jedem abgeleiteten Window überladen werden muß.
class Window{

...
public:
virtual Window* clone()=0;

....
}
class DefaultWindow: public Window{
DefaultWindow* clone(){ return new DefaultWindow();}

}
...

const Window* getNewWindow( const& Window oldWindow){

return Window* newWindow= oldWindow.clone();
  • MOVED TO... nun ist es zu Laufzeit möglich den Konstruktur zu wählen
  • daher spricht man bei dieser Variante der Fabrikmethode gerne vom virtuellen Konstruktor

Python spezifisch

Die Anwendung des clone Protokolls hilft uns hier weiter.
>>> import cloner
>>>class DefaultWindow( cloner.Cloner ):
... pass

>>> a=DefaultWindow()
>>> dir (a)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__',

'__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__',

'__weakref__', 'clone']
>>> b=a.clone()
>>> a == b

False
>>> print a
<cloner.DefaultWindow object at 0x40356eac>
>>> print b

<cloner.DefaultWindow object at 0x403a706c>
>>> type (a) == type (b)

True
  • Anmerkungen
    • dir(a): Dictionary von a
    • a==b: sind die beide Objekte identisch; besitzen sie die gleiche Adresse
    • type (a) == type (b) : sind die beiden Objekte vom gleichen Typ

_getattr_ und _setattr_ hook

  • ähnlicher der
    __init__
    Methode beim Instanziieren eines Objekts, wird beim Zugriff auf ein Attribut eines Objekts gegebenfalls intern die
    __getattr__
    aufgerufen
  • Python unterscheiden nicht zwischen Methoden und Variablen eines Objekts, sondern bezeichnet diese als Attribute
    MOVED TO...jeder Zugriff auf das Objekt stösst implizit eine Prozessieren der
    __getattr__
    Methode an, falls das Attribut nicht explizit aufgelöst werden kann

Es macht einen Unterschied, ob auf das Attribut lesend oder schreibend zugegriffen wird. Ein kleines Beispiel soll dies verdeutliche.

class TestClass:

def __init__(self):
self.__notPrivate=10

def __getattr__(self,attrib):
print "__getattr__: " , str( attrib )
return attrib
  • Nutzung
>>> test= TestClass()

>>> dir (test )
__getattr__: __members__
__getattr__: __methods__

['_TestClass__notPrivate', '__doc__', '__getattr__', '__init__', '__module__']
  • mittels dir(test) werden die Attribute
    TestClass__notPrivate
    und
    __getattr__
    im dem Objektdictionary nachgeschlagen
>>> print _TestClass__notPrivate
10
>>> test.vier
__getattr__: vier

'vier'
  • das Attribut _TestClass__notPrivate wird direkt ausgegeben
    MOVED TO... dies Attribut ist nicht privat, sondern nur gemangelt
  • die Attribute vier wird als Fallback im Dictionary gesucht, da es nicht direkt referenziert werden kann

Proxy

  • Ein Proxy ist ein Stellvertreter für eine Komponente, der sich wie die Komponente verhält aber diese um zusätzliche Funktionalität erweitert.
    Klassischerweise leiten sich der Stellvertreter und die Komponente von einer abstrakten Basisklasse ab.
    Aufrufe des Clienten sprechen das Interface, den Proxy, an, der das an seine Implementierung delegiert.
    Exemplarisch kann man dies im Klassendiagramm sehen.
  • Eine Proxy Klasse ist in Python schnell programmiert.
  • Diese soll ein im Initialisizer mitgegebens Objekt obj kapseln und alle lesenden Zugriffe auf deren Attrinute mitprotokollieren.
class CountAccessProxy:

def __init__(self, obj):
self._obj = obj
self.countAccess={}

def __getattr__(self, attrib):
self.countAccess[attrib]= self.countAccess.setdefault(attrib,0) +1
return getattr(self._obj, attrib)
  • angewandt, sofern man die old-style Klassen benützt
>>> import CountAccess

>>> proxy=CountAccess.CountAccessProxy([])
>>> proxy.countAccess
{}
>>> proxy.extend([i for i in range(10)])

>>> proxy.count(3)
1
>>> proxy.index(3)

3
>>> proxy.insert(5,["string"])
>>> for i in proxy:

... print i,
...
0 1 2 3 4 ['string'] 5 6 7 8 9

>>> print proxy[4]
4
>>> print proxy[5:0:-1]

["['string']", '4', '3', '2', '1', '0']

>>> proxy.countAccess
{'count': 2, 'index': 1, '__getslice__': 1, 'extend': 1, '__getitem__': 2, 'insert': 2, '__iter__':

1, '__len__': 1}
  • ALERT! Dies Idiom lässt sich nicht naiv auf new-style Klassen anwenden, da hier nicht notwendigerweise
    __getAttr__
    prozessiert wird.
    Die new-style class Anwendung des Proxy-Patterns ist im Python-Cookbook beschrieben.

Singleton Pattern - old-style class

  • wird nicht nur den Zugriff auf eine Attribute eines Objekts, sondern auch auf die zu erzeugenden neuen Objekte gekapselt, so erhält man vollkommene Kontrolle über die zu kapselnde Implementierung
  • damit lässt sich die Anzahl der Instanzen einer Klasse kontrollieren
class Singleton:

""" A python singleton """

class __impl:
""" Implementation of the singleton interface """
def getId(self):
""" Test method, return singleton id """
return id(self)

# storage for the instance reference
__instance = None

def __init__(self):
""" Create singleton instance """
# Check whether we already have an instance
if Singleton.__instance is None:
# Create and remember instance
Singleton.__instance = Singleton.__impl()

def __getattr__(self, attr):
""" Delegate access to implementation """
return getattr(self.__instance, attr)

def __setattr__(self, attr, value):
""" Delegate access to implementation """
return setattr(self.__instance, attr, value)
  • die Singleton Klasse fungiert als Proxy, die alle Aufrufe an die innere Klasse __impl delegiert
  • da Singleton.__instance eine Klassenvariable ist, kann im Objektinitialisierer
    __init__()
    sichergestellt werden, daß es nur eine Implementierung - je Klasse - geben kann
  • getattr(self.__instance, attr) ist äquivalent zu self.__instance.attr
  • Anwendung:
>>> handler1= Singleton()

>>> handler2= Singleton()
>>> print "Handler: ",id (handler1 ), "Implementation: " ,handler1.getId()

Handler: 1077243884 Implementation: 1077243436
>>> print "Handler: ",id (handler2 ), "Implementation: " ,handler2.getId()

Handler: 1077243564 Implementation: 1077243436
  • MOVED TO... die Id des Handlers ändert sich, während die Id der Implementierung identisch bleibt

Funktionsobjekte

  • Python kennt keine case Anweisung
  • dies stellt aber keinen Nachteil dar, den die Kombination von Dictionaries und Lambda- Funktionen (anonyme Funktionen) erlaubt mächtigere Konstrukte
>>> select={

... "+": (lambda x,y: x+y),

... "-": (lambda x,y: x-y),

... "*": (lambda x,y: x*y),

... "/": (lambda x,y: x/y) }

>>> for i in ("+","-","*","/"):
... select[i](3,4)

...
7
-1
12
  • die Lambda-Funktionen sind Funktionskörper ohne Namen
  • sie verkörperen in diesem Anwendungsfall eine Strategie, wie Werte verrechnet werden sollen

Strategy Pattern

  • Strategien stellen Familien von Algorithmen dar, die in Objekten gekapselt sind, so daß sie zu Laufzeit ausgetauscht werden können ( vgl. Strategy Pattern )

Anwendung

  • sortiere eine Liste von Strings nach verschiedenen Kriterien - Prädikaten
compare= {"lt": lambda x,y: cmp(x,y),                         # lexikograpisch

"gt": lambda x,y: cmp(y,x),

"ltLen":lambda x,y: cmp(len(x),len(y)), # Anzahl der Zeichen

"gtLen":lambda x,y: cmp(len(y),len(x)),

"igCaseLt":lambda x,y: cmp(x.lower(), y.lower()), # lexikographisch, nicht case sensitive

"igCaseGt":lambda x,y: cmp(y.lower(),x.lower()),

"numValueLt":lambda x,y: cmp(int(x),int(y)), # numerische Wert des Strings

"numValueGt":lambda x,y: cmp(int(y),int(x)),

}
  • wobei cmp eine built-in Funktion ist:
>>> print cmp.__doc__

cmp(x, y) -> integer

Return negative if x<y, zero if x==y, positive if x>y.
  • compare angewandt ergibt
>>> for i in ("lt","gt","ltLen","gtLen","igCaseLt","igCaseGt"):

... print i,
... allStrings.sort(compare.compare[i])

... print ":",allStrings
...
lt : ['Dies', 'Teststring', 'ein', 'ist', 'kurzer', 'nur']

gt : ['nur', 'kurzer', 'ist', 'ein', 'Teststring', 'Dies']

ltLen : ['nur', 'ist', 'ein', 'Dies', 'kurzer', 'Teststring']

gtLen : ['Teststring', 'kurzer', 'Dies', 'nur', 'ist', 'ein']

igCaseLt : ['Dies', 'ein', 'ist', 'kurzer', 'nur', 'Teststring']

igCaseGt : ['Teststring', 'nur', 'kurzer', 'ist', 'ein', 'Dies']

>>> for i in ("lt","numValueLt","numValueGt"):
... print i,

... intList.sort(compare.compare[i])
... print ":",intList

...
lt : ['1', '101', '10101', '21', '3', '3', '4', '56']

numValueLt : ['1', '3', '3', '4', '21', '56', '101', '10101']

numValueGt : ['10101', '101', '56', '21', '4', '3', '3', '1']

Statische und Klassenmethoden

  • old-style Klassen ermöglichen es durch self Methoden an Instanzen zu binden
  • new-style Klassen werden um statische und Klassenmethoden ergänzt
    • statische Methoden: entsprechen den statische Methoden in C++/Java
      • sie können ohne Objektreferenz über den Klassenqualifier aufgerufen werden
      • es wird nur eine statische Methode angelegt
    • Klassenmehoden sind ein neues Konzept
      • sie werden wie statische Methoden aufgerufen und auch nur einmal angelegt
      • der wesentliche Unterschied besteht darin, daß sie an die Klasse gebunden werden

Beispiel

class HelloBase:

def helloStatic(name): print "Hello", name
helloStatic= staticmethod(helloStatic) #1

def helloClass(cls,name): print "Hello from %s" %cls.__name__,name
helloClass= classmethod( helloClass ) #2


class HelloDerived( HelloBase ): pass
  • #1: durch den Ausdrücke #1 wird die Methode helloStatic zur statischen Methode
  • #2: entsprechend wird durch den Ausdruck #2 eine Klassenmethode helloClass definiert
    insbesondere wird hier eine Klassenreferenz durch den Ausdruck cls erzeugt
  • alle Begrüssungen:
>>> helloBase= HelloBase()
>>> helloBase.helloStatic("rainer")

Hello rainer
>>> HelloBase.helloStatic("rainer")
Hello rainer
>>> helloBase.helloClass("rainer")

Hello from HelloBase rainer
>>> HelloBase.helloClass("rainer")
Hello from HelloBase rainer

>>> helloDerived.helloStatic("rainer")
Hello rainer
>>> helloDerived.helloClass("rainer")

Hello from HelloDerived rainer
  • bemerkenswert ist:
    • alle Methoden, ob statisch oder an die Klasse gebunden, könnnen sowohl über die Klasse als auch über die Instanz aufgerufen werden
    • die Methode def helloClass(cls,name) wird auf die richtige Klasse abgebildet

Singleton Pattern - new-style class

  • beim Instanziieren einer Klasse wird mittels
    __new__
    ein neues Objekt angelegt, das dann mittels
    __init__
    initialisiert wird
  • folgender Automatismus wird angestossen
    1. die statische Methode
      __new__()
      der Klasse wird aufgerufen. Diese erhält als erstes implizites Argument ( vgl. self ) die Klasse als Argument und gibt eine neue Klasseninstanz zurück
    2. der anschließende Aufruf der Methode
      __init__()
      initialisierte das gerade instanziierte Klassenobjekt
  • TIPdiesen Prozeß, den Speicher erst mittels
    __new__()
    bereitzustellen und mit
    __init__()
    zu initialisieren, entspricht dem Zusammenspiel des Operators new und dem Konstruktor in C++
  • die enge Verwandheit mit C++ unterstreicht das Code Beispiel
newThing = X.__new__(X, *a, **k)

if isinstance(newThing,X):
X.__init__(newThing,*a,**k)
  • denn,
    • __new__()
      entspricht dem operator new
    • __init__()
      enspricht dem Konstruktor in C++, der auch in C++ erst prozessiert wird, falls Speicher angefordert werden konnte
  • Beispiel
class Minimal(object):             #1

def __new__(cls): #2
print "__new__ :",cls
return object.__new__(cls) #3

def __init__(self): #4
print "__init__ :", self
  • #1: erzeuge eine new-style Klasse
  • #2: übergib die Klasse
  • #3: rufe den Klasseninstanziierer der Basisklasse auf und gib eine fertige - nicht initialisierte - Instanz zurück
  • #4: wird direkt durch #3 angestossen um das Objekt zu initialisieren
Die Ausgabe dazu:
>>> Minimal()
__new__ : <class 'NewClass.Minimal'>

__init__ : <NewClass.Minimal object at 0x4036baac>
<NewClass.Minimal object at 0x4036baac>
  • das Singleton Pattern ( Guido von Rossum )
class Singleton(object):

def __new__(cls, *args, **kwds): #1

it = cls.__dict__.get("__it__") #2

if it is not None: #3
return it
cls.__it__ = it = object.__new__(cls) #3

it.init(*args, **kwds)
return it
def init(self, *args, **kwds): #4

pass
  • Anmerkungen:
    • #1: Aufruf der
      __new__()
      Methode mit voller Signatur:
      • cls: Klassennamen ( notwendig )
      • *args: positionsgebundene Argument ( optional )
      • **kwds: namensgebundene Argumente ( optional )
    • #2: prüfe, ob der Eintrag
      __it__
      im Klassendictionary existiert
    • #3: gib gegenfalls die Basisklasseninstanz zurück, oder erzeuge eine neue
    • #4: initialisiere einmalig die Instanz TIPda bei jedem Aufruf von
      __new__()
      implizit
      __init__()
      prozessiert wird, sollte man diese Methode nicht überladen
  • Anwendung
>>> class MySingleton(Singleton):
... def init(self):

... print "calling init"
... def __init__(self):
... print "calling __init__"

...
>>> x = MySingleton()
calling init
calling __init__
>>> y = MySingleton()

calling __init__
>>> assert x is y #1
>>>
  • init() wird im Gegensatz zu
    __init__()
    nur einmal prozessiert
  • #1 beweist die Objektidentität

Iterator Protokoll

  • Gerne spricht man beim Iterator Protokoll auch von den Komponenten Producer und Consumer.
    • Producer : iterierbare Objekt, das die Elemente zur Verfügung stellt
    • Consumer : Iteratorionscontext, in dem die Elemente konsumiert werden
  • damit selbstdefinierte Klassen in einem Iterationskontext verwendet werden können, müssen sie das Iterator Protokoll umsetzten
  • das Protokoll besteht aus den zwei Konzepten des Iterators und des Iterator Erzeugers
    • der Erzeuger
      __iter__()
      für einen sequentiellen Iterator
    • next(): der Iterator
  • als Beispiel zuerst einen impliziten Iterationskontext
for i in myContainer: pass
  • stösst folgenden Automatismus an
    1. iter( myContainer ) wird implizipt aufgerufen
    2. iter( myContainer ) ruft myContainer.__iter__() auf, falls diese existiert
    3. myContainer.__iter__() gibt einen Iterator zurück
    4. der Iterator iteriert myContainer, bis die Exceptions StopIteration geworfen wird
  • Neben dem impliziten Iterieren, kann man den Iterator auch explizit auf Trab halten.
>>> a=[1,2,3]
>>> i=iter(a)

>>> print i
<listiterator object at 0x40356ccc>
>>> i.next()
1
>>> i.next()

2
>>> i.next()
3
>>> i.next()
Traceback (most recent call last):

File "<stdin>", line 1, in ?
StopIteration

>>>

Iterator Pattern - classical

  • folgende kleine Klasse erzeugt aus einem Charakterstream einen Tokenstream
class TokenIterator:

def __init__(self,string,re_ex ):
self.re_token= re_ex
self.alltokens= self.re_token.split(string)

self.alltokens= self.alltokens[1::2] # 1

self.allTokensIndex=0


def next(self): # 2

try:
token= self.alltokens[ self.allTokensIndex ]

except IndexError: # 3
raise StopIteration # 4
self.allTokensIndex += 1

return token

def __iter__(self): #5
return self
    • #0: splite den String string an jedem Token
    • #1: mich interessieren nur die Token
    • #2: durch die Methode next() wird der TokenIterator zum Iterator
    • #3 und #4: im try Block erzeuge ich einen IndexError, der gemäß des Iterator Protokolls auf einen StopIteration Exception umgebogen wird
    • #5:
      __iter__()
      muß laut Spezifikation eine Iterator bereitstellen; dies ist aber gerade TokenIterator, also self
  • jetzt ist es möglich über die Tokens zu iterieren
>>> import tokenIterator
>>> import re

>>> re_word= re.compile(r"(\w+)")
>>> words= tokenIterator.TokenIterator(open("/etc/passwd").read(), re_word)

>>> for word in words:
... print word
...
root
x

0
0
root
bin
tcsh

....

pop
bin
false
>>> re_num= re.compile(r"(\d+)")

>>> numbers= tokenIterator.TokenIterator(open("/etc/passwd").read(), re_num)

>>> for number in numbers:
... print number
...
0

0
1
1

...

14
76
70
67

100

Iterator Pattern - yield

  • Python 2.3 wurde um das Schlüsselwort yield erweitert
  • dadurch ist es möglich, zustandsbehaftete Funktionen zu implementieren, die als Generatorfunktionen bezeichnet werden

>>> def IntGenerator():

...     for i in range(4):

... yield i
...
>>> ints= IntGenerator()
>>> type (ints)

<type 'generator'>
>>> ints.next()
0
>>> ints.next()

1
>>> ints.next()
2
>>> ints.next()
3

>>> ints.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?

StopIteration
>>> for i in IntGenerator(): print i,
...

0 1 2 3
>>> a,b,c,d= IntGenerator()

>>> print a,b,c,d
0 1 2 3
  • Anmerkungen:
    • yield liefert den Wert des Funktionsaufrufes zurück
    • die Funktion erzeugt einen Iteratorkontext für die Zahlen 0,1,2 und 3
    • die Ausführung der Funktion wird mit der yield Anweisung eingefroren und beim nächsten Durchlauf wieder aufgenommen
    • die Iteration ist mit der Abarbeitung des Funktionskörpers oder einer expliziten return Anweisung beendet
  • Besonderheiten
    • return Anweisungen dürfen keinen Wert zurückgeben
    • yield darf nicht im try Block einer try - finally Exception Behandlung verwendet werden, da es keine Zusicherung gibt, daß finally prozessiert wird
  • nun kann man den TokenIterator deutlich kompakter als Funktion implementieren
def tokenIterator( string, re_token ):

re_token= re_token
alltokens= re_token.split( string )

for token in alltokens[1::2]:
yield token
  • die Ausgabe entspricht der der Klasse TokenIterator
>>> import re
>>> import tokenIterator

>>> re_word= re.compile(r"(\w+)")
>>> for t in tokenIterator.tokenIterator( open("/etc/passwd").read(), re_word ):

... print t
...
root

...

>>> re_number= re.compile(r"(\d+)")

>>> for t in tokenIterator.tokenIterator( open("/etc/passwd").read(), re_number ):

... print t
...
0
0
1

...

>>>
  • TIP durch die neue Library itertools ist es möglich, die funktionale Programmierung und das Iteratokonzept zu einem mächtigen und performanten Werkzeug zu verbinden

 


Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare