A descriptorok

A descriptorok (leírók) szintén a kedvenc python nyelvi eszközeim közé tartoznak. Teljes boldogságban élhetünk akkor is, ha fogalmunk sincs róluk, de bizonyos helyeken meg tudja szépíteni a programunkat. A dekorátorokhoz hasonlóan nem túl bonyolult dologról van szó, ha valaki használt már Djangót, akkor jó eséllyel találkozott velük, még ha nem is tud a létezésükről.

Djangóban a model mezői általában az adatbázis mezőnek megfeleltethető python típusok:

>>> from pages.models import Page
>>> p = Page()
>>> p.creation_date
datetime.datetime(2010, 8, 31, 10, 7, 30, 909404)
>>> type(p.creation_date)
<type 'datetime.datetime'>

Általában... nézzünk csak meg azonban egy ForeignKey típusú mezőt - ha nem rendelek hozzá értéket, akkor látszik a turpisság:

>>> p.author
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/lib/python2.6/site-packages/django/db/models/fields/related.py", line 288, in __get__
    raise self.field.rel.to.DoesNotExist
DoesNotExist

Hmm, milyen __get__? Nezzük csak meg jobban, mi is a author mező:

>>> p.__class__.__dict__['author']
<django.db.models.fields.related.ReverseSingleRelatedObjectDescriptor object at 0x25a9710>
>>> dir(p.__class__.__dict__['author'])
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__',
 '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__set__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'field']

És igen, megvan a tettes, aki nem egy sima mező, hanem egy descriptor, akinek már van __get__ (és esetünkben __set__) metódusa is. Na lássuk akkor mik is azok a descriptorok!

Minek?

Mire jók a descriptorok? Tegyük fel, hogy van egy olyan attribútumunk, amit nem szeretnénk, ha a kliens közvetlenül változtatna, változtatás előtt ellenőrizni akarjuk a beállított értéket. Vagy például egy függvény kimenetét szeretnénk úgy láttatni, mintha egy attribútum lenne. Ezt a descriptorok, illetve azok egy fajtája, a property-k segítségével tehetjük meg.

A descriptor protokoll nagyon egyszerű: a descriptor maga egy (új típusú, azaz object-ből származó) objektum, ami rendelkezik a __get__, __set__, illetve __delete__ metódusok egyikével. Ha egy (új típusú) python objektum egyik attribútuma egy ilyen descriptor, akkor annak lekérésekor/beállításakor/törlésekor értelem szerűen a megfelelő függvény hívódik meg a fentiek közül.

Pontosan hogyan is történik a dolog?

Amikor ojjektum.attr értéket lekérjük, akkor semmi varázslatot nem csinál a python, csak meghívja az ojjektum.__getattribute__('attr') metódust. (Értékadásnál a __setattr__, törlésnél pedig a __delattr__ fut le.) Ennek az alapértelmezett implementációja megnézi, hogy létezik-e az adott objektumnak, a hozzá tartozó osztálynak, illetve valamelyik ősosztálynak a megadott nevű attribútuma - amik egyébként a __dict__ nevű dictionaryben (ennek mi a magyar neve?) csücsülnek -, ha megtalálta, akkor megnézi, hogy ennek a valaminek van-e __get__ metódusa, és ha van azt hívja, ha nincs, akkor egyszerűen visszaadja a talált valamit. Ha nem talált semmit, akkor pedig AttributeError exceptiont dob. A legszebb az egészben, hogy a __getattribute__ és a barátai is mind mind felüldefiniálhatóak, és lecserélhetőek saját implementációra.

Azért van egy kis csavar a dologban: a descriptorokat osztály szinten kell az attribútumokhoz rendelni, így egy konkrét descriptor objektum egy adott osztályhoz, és nem egy konkrét példányhoz tartozik, és a descriptor metódusai magához a descriptorhoz tartoznak, és nem pedig az objektumhoz, ami a descriptort tartalmazza, ezért a metódusok első (self) paramétere maga a descriptor, és a második az objektum, amin keresztül a descriptort elérjük, illetve a __get__ esetében a harmadik a tulajdonos osztály. Buta példa:

class ProbaDescriptor(object):

    def __get__(self, instance, owner):
        # az owner itt a tulajdonos osztaly
        # az instance pedig az objektum
        # (vagy None, ha az osztaly attributumat kerdeztek le)
        return 42

    def __set__(self, instance, value):
        if value is not 42:
            raise ValueError('csak a 42 a jo!')

class A(object):
    proba = ProbaDescriptor()

>>> a = A()
>>> a.proba
42
>>> a.proba = 43
...
ValueError: csak a 42 a jo!
>>>

Propertyk

Persze ha jobban belegondolunk, mi a fenének gyártanánk mi descriptor objektumokat, mikor csak egy getter és egy setter függvényt szeretnénk csinálni valamelyik attribútumunkhoz, külön osztály, meg trükköznünk kell, hogy valami osztályattribútum segítségével tároljuk információkat a példányokról, ez szenvedés (és az igazságtól nem járunk messze egyébként :) ). Szerencsére python 2.2-től kezdve rendelkezésünkre áll a property nevű függvény (illetve 2.5-től dekorátor), melynek segítségével a getter és setter függvényeket egyszerűen össze tudjuk gyúrni egy descriptorrá. Buta példa, a property függvényt dekorátorként is használva:

class PropProba(object):

    __a = 1
    __b = 2

    # csak getter van, egyszeruen dekoratorkent
    @property
    def c(self):
        return self.__a + self.__b

    # getter, setter es torlo, fuggvenykent
    def getA(self):
        return self.__a

    def setA(self, value):
        self.__a = value

    def delA(self):
        self.__a = 1

    a = property(getA, setA, delA)

    # dekoratorkent, eloszor a gettert kell dekoralni, 2.6-os pythontol
    @property
    def b(self):
        return self.__b

    @b.setter
    def b(self, value):
        self.__b = value

    @b.deleter
    def b(self):
        self.__b = 2

>>> x = PropProba()
>>> x.a = 12
>>> x.b = 23
>>> x.c
35
>>> del x.b # x.__b = 2
>>> x.c
14
>>> x.__class__.__dict__['a'].fset(x,42)
>>> x.a
42
>>>

Sokkal egyszerűbb azért így.

Mire jó mindez? Propertyk, illetve descriptorok segítségével cache-elhetünk attribútumokat, szedhetjük őket közvetlen adatbázisból, vagy számíthatjuk értéküket más adatok alapján, validálhatjuk az értékadásokat, és mindezt úgy, hogy a felhasználó (mármint aki az API-nkat használja) egyszerűen attribútumokat kérdez le, illetve állít be. Ahogy a dekorátorok, a descriptorok sem hoznak be semmi szuperújat, de szebbé tehetik a programunkat.

01szept.

5 Comment for A descriptorok

  1. dzuz says:

    szia, köszi az eddigi cikkeket, szerintem jók és nem mellékesen hasznosak is, azt csiripelték a dávid nevű verebek, hogy nem mindig van ötleted, hogy miről szeretnél írni :) én szívesen olvasnék az űrlapkezelésről és pl. arról, hogy a django alatt, hogyan lehet legszebben beillesztgetni JavaScriptet, vagy, hogy a cachelés-t hogyan szoktátok megoldani :)

    • RePa says:

      hello,

      elsodlegesen az idom a keves :) de mindenkepp kivancsi vagyok, hogy a kedves olvasok milyen cikkeket latnanak szivesen, hogy ne feleslegesen irkaljak mindenfelet :)

      szoval nem igerek semmit, de igyekszem szem elott tartani a kivansagokat

      • dzuz says:

        köszi :) én kb. 1 hónapja kezdtem djangozni, de közben értelemszerűen a Python is egyre jobban megtetszett, régebben is már foglalkoztam vele, de akkor mivel nem volt semmi konkrét feladatom amit ebben kellett volna megoldanom lassan a feledésbe veszett... :( Most a Django elkezdett érdekelni, elsősorban mint Symfony és PHP alternatíva és mivel nagyon hatékonynak és nem mellékesen szépnek tűnik (ha szabad ilyet írni egy prog. nyelvről), ezért mindenképpen használni szeretném ahol csak lehet :) A cikkeknek így is örülök, arról írj amit hasznosnak tartasz és ami örömet okoz neked is, mert minden hasznos ami tapasztalaton alapul :)

  2. gricso says:

    A magam részéről csak örülök, hogy írsz, ami éppen az eszedbe ötlik. Sokat tanultam az eddigi leírásokból is! - Az egyetlen, amit itt-ott bővítenék: a további irodalom ajánlása. Nekem sokat segít, ha kapok útbaigazítás, hogy hova-tova, ha valami komolyabban felkeltette az érdeklődésemet.

    Köszönettel, Dávid

Szólj hozzá