Metaprogramozás I

Bevezető

A metaclassok nyitják meg a teret a metaprogramozás előtt kedvelt nyelvünkben. Megértésükhöz szükséges néhány fogalmat és építőelemet tisztázni, mert nem a metaclassok a nehezek magukban, hanem az a több tucat pythonos építőkocka sok, amit össze kell gyűjteni. RePa dekorátorjaihoz és descriptorjaihoz hasonlóan semmi olyat nem tesznek lehetővé amit vagy a megszokott procedurális vagy objektum orientált eszközeinkkel ne tudnánk kifejezni vagy megoldani. Főképp APIk létrehozását teszik áttekinhetővé (pl Django ORM és newforms API). Természetesen Djangóban mindig ott figyelnek a háttérből és ha valaki már dolgozott adatbázis modellekkel/formokkal, akkor használta őket akarva, akaratlanul.

Minden ojjektum és type

Úgy marketingelik a Python nyelvet, hogy minden objektum benne, és 100%-ig ojjektum orientált nyelv, tehát pl. nincsenek benne primitívek, mint Javában. :) Ne higgyünk el mindent amit olvasunk, járjunk utána. Objektum-e pl egy tetszőleges python modul? Bármilyen "névnek" lekérdezhetjük a típusát a type beépített fügvénnyel. Név alatt értek mindent ami az adott névtérben elérhető :). Ezek elég szerteágazóak: változók, függvények, modulok, osztályok, objektum példányok és mindenki aki lemaradt.

>>> import sys
>>> print type(sys)
<type 'module'>
>>> print sys.__class__
<type 'module'>

>>> print type(3)
<type 'int'>
>>> print type(u"123")
<type 'unicode'>
>>> print type(lambda x:x+1)
<type 'function'>

Azaz minden modul objektum, típusuk (osztályuk) module. Nézzük mi a module típusa.

>>> print type(type(sys))
<type 'type'>

Tehát a típusa type. Ez minden típus őse. A type azonban nem csak típusok lekérdezésére szolgál, hanem létrehozására is. Ha egy objektumot adunk neki paraméterként, a típusát adja vissza, amit rögtön példányosíthatunk, azaz létrehozhatunk belőle egy objektumot.

>>> mystring = type("string")()
>>> print mystring.__class__
<type 'str'>
>>> print len(mystring)
0

Azaz létrehoztunk egy üres stringet, úgy hogy egy meglévő string típusát megkaptuk és meghívtuk mint függvényt, azaz a típus konstruktorát használtuk. A visszakapott érték egy üres string.

A függvényhívás operátor pythonban

Álljunk meg egy szóra itt, érdemes megjegyezni hogy pythonban nincs new operátor, amivel osztályokat példányosíthatnánk. Az osztály meghívjuk mint egy függvényt, ezzel hozunk létre új élő objektumot. A python nyelv igazából csak a callable, azaz meghívható fogalmat ismeri.

  • Meghívhatunk egy függvényt, lefut, esetleg visszatérési értéket is ad.

  • Meghívhatunk egy osztályt, ezzel példányosítunk, létrehozunk egy objektumot, ez az objektum a függvény visszatérési értéke.

  • Meghívhatunk egy példányt is, ekkor a legtöbb esetben hibát kapunk, hacsak nem tesszük callable-é a példányt, vagy osztályát. Tegyük:

Érezzük át a pythont a szabályok mögött, az hogy valami meghívható ismét egy protokoll, amely a zárójelek használatán, a paraméterek átadásán és a visszatérési értéken alapszik.

class CallableKlassz(object):

    def __call__(self,*args):
        " Csak ennyit kell tenned ahhoz hogy meghívható lehess. "
        print 'From now on, you can call me callable!'
        print ",".join(args)

>>> k = CallableKlassz()
>>> k('1','2','3')
From now on, you can call me callable!
1,2,3
# a callable() függvény megmondja hogy egy adott név meghívható vagy sem
# k biztosan callable, ezért nem keletkezik hiba.
assert callable(k)

Van az úton egy buktató mielőtt tovább mehetnénk, ami a 2.2es python körül történt. Ezekről most muszáj szót említeni, hogy senki ne essen a csapdájukba.

Régi vs. Új osztályok

A 2.2es python egyesítette a típus rendszerét, így feloldottak egy ellentmondást, és létrehozták az új stílusú osztályokat. A régi stílusúak kompatibilitási okokból a 2.X verziókban működnek, 3-masban már tilos, többek között ezért sem fut még Django a 3-mas vonalon.

# Régi típusú osztály
class RegiKlassz:
    pass

# Régi típusú osztály ismét
class RegiKlassz():
    pass

# Új típusú osztály
class UjKlassz(object):
    pass

# ez is új típusú
class UjKlassz2(Shape,Rectange,PrintAbleRectangle):
    pass

Tehát szintatikailag csak annyi a különbség hogy explicit megmondjuk, hogy van ősünk. Aki csak teheti kerülje a régi írásmódot, mellesleg pedig nem is működik az itt leírt csodák jelentős része régi típusú osztályokkal. Csak ennyit jegyezzünk meg: explicit módon mindig jelezzük hogy kitől vagy kiktől öröklünk.

Tehát új osztályokra építve nézzük tovább a típusrendszert.

A type függvény

A kitérő után vissza a type használatára. A type egy beépített (builtin) függvény, amivel futási időben létrehozhatunk egy típust (mondhatjuk egy osztályt). Ha létrehoztuk a típust, akkor már példányosíthatjuk is. Három paramétere van:

type(name, bases, dict)
  • A name az új osztályunk neve, természetesen string. (2.X verzióban csak csak bytestring, nem lehet unicode, de ne is akard. :) )

  • A bases egy zárt lista (tuple), ami az ősöket listázza, lévén hogy pythonban engedett a többszörös öröklés.

  • Végül dict egy dictionary, ami a létrejövő osztályunk tagjait tartalmazza, mind a mezőket és metódusokat.

Szereljünk össze egy összetettebb osztályt, hogy lássuk milyen egyszerű is ez. Származtassunk egy osztály a lista típusból (list), legyen saját konstruktora, egy metódusa és egy változója, ami egy jól ismert konstans lesz jelen esetben.

def method1(self):
    print 'method1()'

def konstruktor(self):
    print "new instance created()"
    self.foo = "bar"

my_dynamic_made_class = type('MyDynamicMadeClass',
                             (list, object),
                             {'PI':3.14, 'method1':method1,
                              '__init__':konstruktor})

# Osztálya:
# <class '__main__.MyDynamicMadeClass'>
print my_dynamic_made_class

# Azért működik mindkettő mert PI elérhető
# mind az osztályon, mind a példányon keresztül.
# Ez a __getattr__ metódus bűne :)
print my_dynamic_made_class.PI
print my_dynamic_made_class().PI

# Példány metódus működik
my_dynamic_made_class().method1()
# Osztályon keresztül nem hívhatjuk, mert a statikus és
# osztály metódusok már másképp működnek mint a példány metódusok.
# El tudjuk érni, azaz látjuk, csak nem úgy működik ahogy szeretnénk.
try:
    my_dynamic_made_class.method1()
except TypeError:
# TypeError: unbound method method1() must be called with
# MyDynamicMadeClass instance as first argument (got nothing instead)
    pass

peldany = my_dynamic_made_class()

# kíirja a "bar" szót
print peldany.foo

# és mivel listából származtattuk ez is megy
peldany += [1,2,3]

# és iterálható is, mert listából származtattuk...
for i in peldany: print i

Ezt az osztály dinamikusan szereltük össze. Azaz nincs deklarálva, mondhatnánk nincs statikus definíciója. Tegyük meg ugyanezt a régi, jól bevált statikus módon:

class MyOldClass(list,object):
    PI = 3.14

    def __init__(self):
        print "new instance created()"
        self.foo = "bar"

    def method1(self):
        print 'method1()'

Látszik hogy az utóbbi jóval tömörebb, érthetőbb és szebb is. A type függvénnyel tehát két dolgot is tehetünk:

  • megkaphatjuk egy tetszőleges név típusát válaszul

  • létrehozhatunk új típust

A kapott értékek típus referenciák. Ezeket továbbpasszolhatjuk függvényeknek, vagy példányokat gyárthatunk belőle.

Látjuk azt is, ha a fordított irányból nézzük, hogy miből is áll egy python osztály.

  • egy név, illik nagybetűvel (hogy PEP8 kompatibilis maradj) kezdeni az összes szavát (EzEgySzepNevuOsztaly)

  • az ős osztályai, melyek futási időben is bejárhatóak ( OsztalyNeve.__bases__ lista, vagy OsztalyNeve.__base__ az elsőt adja)

  • egy szótár (dictionary) amely az összes elemét (mezők és metódusok) tartalmazza, ez a __dict__.

Még egy elemre lesz szükség, hogy a metaclassok könnyen elmagyarázzák magukat ezek pedig a mágikus mezők.

Magic Methods és Magic Fields

Aki látott már PHP-ben OOP-t az akár át is ugorhatja ezt a kitérőt. A mágikus nevek úgy működnek, hogy bár nem rezervált kulcsszavak, azonban ha osztályon belül használod őket speciális módon fogja őket meghívni vagy kezelni a python futtató. Tehát osztályon belül igenis foglalt a nevük, de modulban használhatóak ezek a nevek. Azt tudjuk hogy pythonban nincsenek privát mezők, minden publikus, ha éreztetni akarjuk egy változóról vagy metódusról hogy védett, akkor _ alulvonással kezdjük a nevét. Jogosan érezhetjük hogy ami két alulvonással kezdődik az nagyon szigorúan el lett rejtve. :) A jelölési szabály: dupla alulvonás elé/mögé. Amivel már találkoztunk, azaz az __init__, ami egy új példány létrehozásánál automatikusan hívódik meg. Az előbb láttuk a __call__-t is. Alsóhangon két tucat létezik itt.

A kövtkező részben látni fogjuk hogy a metaclassokhoz bevetéséhez mindössze meg kell neveznünk a metaclasst, természetesen a __metaclass__ osztály attribútummal.

Erre a pár részletre volt szükség ahhoz, hogy a következő fejezetben a két hiányzó elemet a helyére illesszük és meg is vagyunk a metaclassokkal. Azt tudnotok kell, hogy ez már metaprogramozás volt (dinamikusan építettünk osztály), ami ez után következik, az csak hab a tortán, némi szintatikai porcukorral meghintve.

Következik hamarosan a II. fejezet, ahol bevetjük végre a __metaclass__-t.

25szept.

Szólj hozzá