Monday, July 24, 2006

Megoldas a metaclasses

Ujra vissza az elozo problemahoz. A metaclassok tulajdonkeppen olyan osztalyok amiknek a peldanyai is osztalyok. Ez igy elsore lehet furcsan hangzik mert az ember ugy tanulta hogy az osztalyok peldanyai az objectek. Objectet az ember ugy csinal hogy van egy osztalya ami sablonkent mukodik es az alapjan csinal belole peldanyt. Ugyanazt meg lehet tenni osztallyal is, azaz egy olyan masfajta osztaly kell ami maga is osztalyt ir le. Ez a masfajta osztaly a metaclass. Pythonban a legnyilvanvalobb, beepitett metaosztaly a type. De ilyen a types.ClassType is. Ez elobbi a beeptitett osztalyok metaclassa, az utobbi pedig a user altal gyarott osztalyoke. Pythonban maguk az osztalyok is objectkent vannak megvalositva, ugyanugy (legalabbis az en problemam szempontjabol) jonnek letre (egy metaclass peldanyaikent), mint a normal objectek. Ez pedig megoldast jelent szamomra, mert nincs mas dolgom mint csinalni egy uj metaosztalyt, aminek a konstruktoraba elrejtem az interface ellenorzo kodomat. Ebbol a metaosztalybol csinalni egy uj peldanyt, azaz egy normal osztalyt. Ez lesz az Interface osztaly aminek a metaosztalya az InterfaceType. Az Interface mar egy teljesen normalis osztaly, ebbol leszarmatathatok barmilyen interfacet. pl.:


class ITest( Interface ):
def test(self, t): pass


Mivel az Interface osztaly az InterfaceType konstruktorat orokolte (metaosztalyoknak is van konstruktoruk ami akkor fut le ha peldanyt hozunk letre belole, azaz normal osztalyt a metaosztalybol), az ITest pedig az ovet. Igy az ITest osztaly megkrealasakor, le fog futni a metaosztalyomban levo ellenorzo kod. Hangsulyozom hogy osztaly letrehozasakor, nem osztaly peldanyositasakor. Ezutan ezt az interfacet "implementalom", amihez ugyanugy az oroklest hasznalom.


class Test( ITest ):
def test(self, t):
return t



A Test osztaly letrehozasakor szinten lefut az ellenorzes. Mi is van ebben az ellenorzesbe. Eloszor is lekerdezi az aktualis osztaly metodusait. Jelen esetben a Test krealasakor ez a 'test' metodus. Lekerdezi az osztaly direkt oseit. Ha ennek az osnek az ose Interface osztaly akkor o maga egy Interface. ITest jelen esetben. Igy el tudom donteni hogy a Test osei kozul melyik az interface mert nem biztos hogy csak ITestbol fog szarmazni, lehet ott mas nem interface osztaly is. Tehat megvannak az implementalando interfacek (ITest vagy Interface osztaly letrehozasakor nem fog ilyet talalni ekkor nem is ellenoriz semmit). Az interfacek metodusait egyenkent lekerdezem, jelen esetben megkapom az Itest.test(self, t)-t, es megnezem hogy ez egyenlo-e (marmint 2 fuggveny referencia) az aktualis osztaly a Test, ugyanilyen nevu (mert hat biztos hogy van neki ilyen hiszen orokolte, kerdes hogy a user feluldefinialta-e) metodusaval. Ha a 2 referencia megegyezik akkor csak sima oroklesrol van szo, ha kulonbozo akkor felul lett definialva, azaz a user kozvetlenul beleirta a kodba, tehat implementalta. Ezutan leellenorozm meg a fuggveny parametereinek a szamat is, ha nem stimemel ugyanugy assert errorozik (azert assert, mert az letilthato egy parancsosoros parameterrel ha pl release verzional nem akarjuk ellenorizgetni). Szoval ha en azt mondom hogy class Test( ITest, Base1, Base2 ), es az ITest interfacebol szarmazik, a Base1/2 pedig normal osztaly, akkor az ITestben levo metodusokat koteles vagyok feluldefinialni.

Es akkor jojjon a kod:


from types import MethodType

def getMethods( clz ):
return [i for i in dir(clz) if type(getattr(clz, i)) == MethodType]

class InterfaceType(type):

def __new__(cls, name, bases, dct):
return type.__new__(cls, name, bases, dct)

def __init__(cls, name, bases, dct):
super(InterfaceType, cls).__init__(name, bases, dct)

for b in bases:
if b.__base__.__name__ == 'Interface':
imet = getMethods( b )
for m in imet:
assert getattr( cls, m ) != getattr( b, m ), 'WARNING: method %s not implemented in %s' % ( m, cls.__name__ )

assert getattr( cls, m ).func_code.co_argcount == getattr( b, m ).func_code.co_argcount, 'WARNING: method %s not correctly implemented in %s (parameter mismatch)' % ( m, cls.__name__ )

Interface = InterfaceType('Interface', (), {})


Mivel metaclassrol van szo lathato hogy konstruktorban nem self, hanem cls, azaz metaosztaly peldanyra mutato referencia van. A __new__ az __init__ elott hivodik meg es az uj osztaly peldanyaval ( ami most szinten osztaly mivel metaclassbol hozza letre ) ter vissza ('type' osztaly factorykent mukodik). Elofordulhat hogy ugy jon letre az osztaly hogy az __init__ nem hivodik meg pl picklevel serializaljuk be, ilyenkor az ellenorzes sem fog lefutni (normal osztaly betolteskor szerintem le kell mindig). De a 2 cls parameter nem ugyanaz a 2 fuggvenyben. A __new__ban a self-el analog a metaosztaly sajat referencia van, mig a konstruktorban az aktualis metaosztaly peldany, azaz normal osztalyra mutato referencia. A for loopban a direkt ososztalyokbol kivalasztjuk azokat akiknek az ose az Interface, es ezutan kovtkezik a metodusok osszehasonlitasa. Az utolso sor nagyon fontos, Interface = InterfaceType('Interface', (), {}), itt hozok letre az InterfaceType metaosztalybol egy peldanyt, azaz egy normal Interface osztalyt.

Itt pedig a test kod


from interface import Interface

class ITest( Interface ):
def test(self, t): pass

class Cloneable( Interface ):
def clone(self): pass

class Test( ITest, Cloneable ):
def lofasz(self):
return t

def test(self, t):
return t

def clone(self):
print 'clone'

class Test2( Test):
pass


A modszer meg erosen kiserleti, tesztelest igenyel, de elsore nem latok olyan problemakat mint az elozo kettonel.

Sunday, July 23, 2006

Masodik nekifutas

Elozo postomban leirt modszer nemigazan hasznalhato addig amig nincs osztaly decorator. Most mashogy kozelitettem meg a problemat es a class static valtozojaba taroltam a interfaceket. pl.:


class ITest:
def test(): pass

class Test:
__implements__ = ( 'ITest', 'interface_pkg.interfaces.iclone.clone.Cloneable', 'serializable.Serializable' )

def sample(self): pass
def serialize(self): pass
def clone(self): pass
def test(self): pass


A syntax hasonlo mint zopenal (de azt nem tudom hogy ellenorzi). Lathato hogy modul/csomag nevet fel kell tunetni az osztaly neve elott. Ha nincs odairva semmi (mint az ITest-nel) akkor az aktualis fileban fogja keresni az adott osztalyt(interfacet).

Az ellenorzes pedig a kovetkezokeppen mukodik. A modul importalast hookoltam, igy a sajat importerem lefog futni minden import elott (ezert kell torolni az import cachet). Ez az importer ellenorzi az importalando fileban levo osszes osztalyt, megnezi van-e valamelyiknek __implements__ static attributuma. Ha talal ilyet, akkor az abban talalhato interfaceket betolti, es lekerdezi a benne levo metodusokat. Majd megnezi hogy az implementalo oszalyban ezek benne vannak-e. Ha igen akkor visszater a modullal, egyebkent hibat ir es None-t kuld vissza, ami ImportError-t fog okozni.

Hasznalat pedig a kovetkezo. Eloszor interface_checker -t kell importalni, ami hookolja magat. Innentol kezdve ha valamit importalunk akkor azt leellenorzni. Ebbol viszont mar ki lehet talalni mi a problema ezzel a modszerrel. A __main__ modul betoltesenel hamarabb nem tudja hookolni magat (mert legkorabban ott lehet importalni) igy az abban levo osztalyokat nem ellenorzni.

Ezenkivul kicsit olyan haxolos benyomast kelthet a dolog. Azert teszem megis koze, mert a import hookolas egy jo feature, es hata inspiral masokat egyeb problemak
megoldasara. Pl az urlimport - amivel httprol lehet python modult betolteni - is ezt hasznalja. De pl olyat meg nem lattam hogy adatabazisbol lehetne ugyanigy importalni, pedig ez is hasznos lehet.

Ime InterfaceImporter highly experimental verzioja:


class InterfaceImporter:

"""
TODO: check number of parameters
TODO: asserterror
TODO: check main module
"""

disable = False

def __init__(self, path):
print '[init]: at', path

def find_module(self, fullname, path=None):
if InterfaceImporter.disable: return None
print '[find module]: %s at %s' % ( fullname, path )
return self

def load_module(self, fullname):
print '[load module] at', fullname

InterfaceImporter.disable = True

try:
mod = __import__(fullname)

# get all class from the module
classes = [ i for i in dir( mod ) if type(getattr(mod, i)) == ClassType ]

print 'Module %s contains %s class(es)' % (fullname, classes)

try:
for clazz in classes:

clazz = getattr(mod, clazz)

if not hasattr( clazz, '__implements__'):
# no __implements___ field found, skip this class
print 'No interface in %s' % (clazz)
continue

interfaces = getattr( clazz, '__implements__')
print '%s implements %s interface(s)' % ( clazz, interfaces )

iobjes = []
for inf in interfaces:
if inf.rfind( '.' ) == -1:
ipkg = fullname
iname = inf
inf = ipkg + '.' + inf
else:
iname = inf[inf.rindex('.')+1:]
ipkg = inf[:inf.rindex('.')]

iobj = __import__( ipkg )
for isub in inf.split('.')[1:]:
iobj = getattr(iobj, isub )

iobjes.append( iobj )

self.checkInterfaces( clazz, iobjes )

except AssertionError, e:
print "implementation error: %s" % (str(e))
return None
except Exception, ex:
print 'Error:', str(ex)
return None
else:
print 'All method implemented'
finally:
InterfaceImporter.disable = False

return mod

def getMethods( self, clz ):
return [i for i in dir(clz) if type(getattr(clz, i)) == MethodType]

def checkInterfaces( self, clazz, interfaces ):
cmethods = self.getMethods( clazz )
print 'class methods of %s: %s' % ( clazz,cmethods )
for inf in interfaces:
imethods = self.getMethods( inf )
print 'interface methods of %s: %s' % ( inf, imethods )
for i in imethods:
assert i in cmethods, `i` + ' not correctly implemented'

# import hook
sys.path_hooks = [x for x in sys.path_hooks if x.__name__ != 'InterfaceImporter']
sys.path_hooks.append(InterfaceImporter)
sys.path_importer_cache.clear()


Az import hookolasrol bovebben pep-0302-ben lehet olvasni. Itt azt is leirjak hogy milyen protocolt kell kovetnie az importer osztalynak. Ez a protocol dolog olyan a pythonnal mint az interface statictyping nyelveknel. Ez mindossze annyit tesz hogy doksiba leirjak hogy milyen metodusok szuksegesek ahhoz h mukodjon. Ha valamit kihagyunk majd runtime kiderul, de amig hapog es duck-kent viselkedik addig duck :)
Egyebkent meg unittest rulez.

Friday, July 21, 2006

Python interface

Az interface egy hasznos dolog, kulonosen ha frameworkot tervez az ember. Az adott specifikaciot eloszor interfacekkel irjuk le, majd arra adunk egy konkret implementaciot. Az interface alapjan mas is eltudja kesziteni a meglevovel kompatibilis de eltero implementaciot. A kozos interface pedig biztositja hogy meglevo mas libekkel egyutt tudjon mukodni. Pythonban nincsenek interfacek, es ugy vettem eszre hogy legtobben nem is nagyon tartanak ra igenyt. Ugyanakkor a nagyobb frameworkok mint zope, kifejlesztettek interfacekre sajat modszert. Megprobaltam en is irni egy egyszeru decoratort ami ellenorzi hogy egy osztaly implementalta-e maradektalanul a megadott interfacet.

Egy ehhez hasonlot akartam eloszor csinalni, de class decorator (meg?) nincs pythonban igy a syntax ezt nem engedi meg.


@implements( [IInterface1, IInterface2] ) # not supported
class Test:
...


Ezert fuggveny decoratorral probalkoztam. Konstruktort altalban explicit megadja az ember igy erre huztam ra a decorator fuggvenyt. A problema ezzel hogy peldanyositaskor ellenorzi, nem pedig a modul betoltesekor, igy csak runtime derul ki ha baj van. (De talan hamarabb mintha metodus hivasnal jonnenk ra hogy nem letezik az adott fuggveny)


class ITest:
def testMethod(self): pass

class Cloneable:
def clone(): pass

class Test:

@implements( (ITest, Cloneable) )

def __init__(self, s, i):
print s, i

def testMethod(self):
print 'implemented'

def clone(self):
print 'clone test'

t = Test( 'test', 1 )
t.testMethod()
t.clone()


A konstruktorra kell hattatni a decoratort, ami visszad egy olyan fuggvenyt ami leellenorzi az implementaciot es meghivja az eredeti konstruktort. A tovabbi problema ezzel hogy minden egyes peldanyositaskor lefut az ellenorzes, es feleslegesen lassit. Ezt esetleg meg lehetne oldani egy global release/debug flaggel amit ha kikapcsol az ember akkor skipeli az ellenorzest. Ha valami static initializacios blokkba el lehetne helyezni hasonlo modon az ellenorzest es ugy a modul importalasanal le tudna ellenorizni akkor hasznalhato lenne. Ha valakinek van erre hasznalhato otlete az irja meg plz.

A decorator amit en hasznaltam a kovetkezo:


def implements( infaces ):
"""
implements decorator
usage:
class Sample:
@implements( (IInterface1, IInterface2, IInterfaceN )
def __init__(self, args):
...
warning: this will check the implementation at runtime, before every creation of the instance
"""
def f( init ):

from types import MethodType

def getMethods( clz ):
return [i for i in dir(clz) if type(getattr(clz, i)) == MethodType]

def checkInterface( clazz ):
cmethods = getMethods( clazz )
for inf in infaces:
imethods = getMethods( inf )
for i in imethods:
assert i in cmethods, `i` + ' not correctly implemented'

def new_init(ref, *args, **kargs): # this will be the new constructor
checkInterface( ref.__class__ )
return init(ref, *args, **kargs)

return new_init

return f

Wednesday, July 12, 2006

Python 2.5 beta

Megjelent a python2.5 masodik betaja. Csak most volt idom kiprobalni par uj featuret ezert nemirtam az elsorol. Errol viszont most egy kicsit bovebben fogok.

Nezzuk milyen ujdonsagokat rejteget szamunkra. Elszor is van amd64 portja ami tok jo (lesz majd). Mostmar van a c-s '?:'-hez hasonlo felteteles kifejezes bar teljsen mas syntaxal. "x = cond ? 1 : 0" helyett "x = (1 if cond else 0)". A zarojel nem kotelezo de azt ajanljak(nagyon helyesen) hogy hasznaljuk a jobb olvashatosag miatt. Nem tudom eldonteni hogy ez jo vagy rossz, eddig se volt nehez leirni azt 2 sort, a python pedig sose arrol volt hires hogy kevesebbet kell gepelni (mint pl perl) hanem hogy rendkivul jol olvashato a kod. Eloszor arra gondoltam hogy c-s verziot kellett volna akkor mar belerakni, mert azt mindenki ismeri es megszokta mar. Viszont az egyaltalan nem illik a python szintakszisahoz. Valoszinuleg azert viszolyogtam eloszor tole mer rogton a perles "expression if (condition)" -re asszocialtam, (Na akkor mar csak az unless hianyozna es kb dobnam kukaba az egesz nyelvet:)) node nem errol van szo ugyhogy lehet hogy hasznosnak fog bizonyulni.

Van egy masik nyelvi feature aminel egyaltalan nem volt kerdes hogy jo-e vagy nem. Megpedig hogy finally megengedett lett exceptionnel egyutt.

Azaz:

try:
print 1/0
except Exception, e:
print e
finally:
print 'finally'


Ez egyertelmuen ql, johetett volna hamarabb is.

Ha mar finally akkor ugylatszik nem alltak meg ennel a developerek hanem tovabbmentek es csinaltak egy meg egyszerubb modszert a clean-up es hasonlo muveletek lefuttatasara. Bejott egy uj kulcsszo a "with" (es "as") aminek segitsegevel az object tulajdonkeppen sajat maga vegezheti a clean-up-ot, eroforras felszabaditast illetve minden olyat amit altalban az ember a finally blockba tesz. Kovetkezokeppen kell elkepzelni:


with kifejezes as valtozo:
valtozo.fgv1
valtozo.fgv2


Az erdekes ebbe az hogy a with block vegen vagyha kivetel keletkezett, tehat mindekeppen lefut egy cleanup amit az osztalyban van elore definialva. Olyan objectet hasznalhatunk with blockba ami tamogatja ezt, un. Context Manager protocolt. Ami egyszeruen annyit jelent hogy egy elore meghatarozott interface metodusaival rendelkezik, de errol majd kesobb.

Hogy vilagosabb legyen ime egy filekezelos pelda:


with open('file.txt', 'r') as f:
for line in f: print line


A muvelet vegen a file szepen lezarodik, anelkul hogy ki kene irni az f.close()-t.

A PEP-ben erre a modszerre tobb peldat is mondanak, pl I/O muvelet mint filekezeles, thread lock hogy ne felejtsunk el releaselni, es ami nekem a legjobban tetszett az a tranzakcio kezeles. Erre fogok most kiterni.

Tehat van egy db tablank akarunk valamilyen muveleteket vegrehajtani rajta, ha pl valamelyik nem sikerul egy kivetel miatt akkor rollbackelje az egeszet egyebkent mehet a commit.

Ezt ezt ehhez hasonlo koddal megtehetjuk.


db = DatabaseConnection()
# begin transaction
with db as cursor:
cursor.insert('first row')
cursor.delete('another row')


De nezzuk mi van a hatterben azaz hogy tudunk olyan osztalyt csinalni ami tamogatja a with-et. Eloszor is ez a bizonyos Context Manager eloirja szamunkra hogy legyen az osztalynak egy __enter__ es egy __exit__ metodusa. Az enter a blockba valo belepeskor hivodik meg es visszaadja azt az objectet amin a muveletet szeretnenk elvegezni. Ez fog az 'as' kulcsszo utan megadott valtozoba eltarolodni. ( masszoval cursor = db.__enter__() ) Kell hogy legyen tovabba egy __exit__(self, type, value, tb) amibe altalaba cleanupot tesszuk. Itt a commitot, rollbackat vagy a db connectionpooling lezarast. Az exit parameterei ugyanaz mint a sys.exc_info(). Innentol kezdve mar haszanhatjuk is a with-et.

A lenti examplet a PEP-bol vettem es egeszittem ki, a DatabaseConnection az ami tudja ezt a bizonyos interfacet, van neki exit es entere. Az enter visszater a cursorral amivel insertelhetunk vagy torolhetunk az adatbzisbol. Az exit pedig ha nincs kivetel akkor commitol, egyebkent rollbackkel. Az exit visszateresi erteke is fontos, ez altalaban false, ami azt jelenti hogy a kivetelt ujradobja, a true (csak a pelda miatt true) pedig teljesen elnyeli.


from __future__ import with_statement # ez elvileg csak a betaban kell, kesobbiekben alapbol importalni fogja

class Cursor:
def insert(self, s):
print 'insert', s

def delete(self, s):
print 'delete', s
raise Exception('Database exception: delete error')


class DatabaseConnection:
def __enter__(self):
self.cursor = Cursor()
return self.cursor

def __exit__(self, type, value, tb):
if tb is None: # No exception
# do commit
print 'COMMIT'
else:
# do rollback
print 'ROLLBACK', value
# release the connection object...
return True #False



db = DatabaseConnection()
# begin transaction
with db as cursor:
cursor.insert('first row')
cursor.delete('another row')



A program kimenete pedig a kovetkezo:

insert first row
delete another row
ROLLBACK Database exception: delete error

Ugyanis a deleteben szandekosan dobtam egy kivetelt, ellenkezo esetben kommitol.

Errol lehet azt is gondolni hogy nem egy nagy dolog, raadasul eddig is siman meg lehett finallyval mostmeg irhatok neki kulon osztalyokat. A dolog lenyege inkabb az szerintem hogy ezekutan a standard runtime osztalyai es valoszinuleg a thirdparty libek nagyresze is tamoatni fogja ezt a modszer ahol ez szukseges, azaz nem nekem kell megirni a cleanupot minden muveletnel hanem az object gondoskodni fog arrol hogy a block vegen lezarja a filet, bezarja a socket kapcsolatot/adatbazist stb. Szerintem tokjo featurenek nez ki. Ja, es van egy contextlib modul amibe olyan decorator van definialva ami leegyszerusiti a context managerek fuggvenybol torteno gyartasat.

Van meg relativ import, es sok mas uj feature is, ezeket mindenki olvassa el a whatsnew-ban.