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
- assert 0, "method is abstract"
- 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;
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();
- 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
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
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}
- 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
- 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
- statische Methoden: entsprechen den statische Methoden in C++/Java
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
- 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 - der anschließende Aufruf der Methode
__init__()
initialisierte das gerade instanziierte Klassenobjekt
- die statische Methode
- diesen 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
>>> 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 da bei jedem Aufruf von
__new__()
implizit__init__()
prozessiert wird, sollte man diese Methode nicht überladen
- #1: Aufruf der
- 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 stelltConsumer
: 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
- der Erzeuger
- als Beispiel zuerst einen
impliziten
Iterationskontext
for i in myContainer: pass
- stösst folgenden Automatismus an
- iter( myContainer ) wird implizipt aufgerufen
- iter( myContainer ) ruft myContainer.__iter__() auf, falls diese existiert
- myContainer.__iter__() gibt einen Iterator zurück
- 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
...
>>>
- durch die neue Library itertools ist es möglich, die funktionale Programmierung und das Iteratokonzept zu einem mächtigen und performanten Werkzeug zu verbinden
Weiterlesen...