Saturday, January 10, 2009

Python - kontrola dostępu do parametrów obiektu i klasy

O kontroli dostępu do składowych klasy pisałem już troszeczkę we wpisie Python - hermetyzacja i enkapsulacja z punktu widzenia programisty C++. Jest to jedna z metod kontroli dostępu jednak na małą skalę. Nadaje się świetnie do sprawowania pieczy nad dostępem do kilku pojedynczych atrybutów. W Pythonie istnieją jednak mechanizmy pozwalające na globalną kontrolę tego procesu. Zobaczmy jak to działa.

Python proponuje tutaj dwa podstawowe rodzaje metod. Pierwszą z nich, którą omówimy, będzie __setattr__(self, name, value). Metoda ta wywoływana jest za każdym razem gdy staramy się przypisać jakiemuś parametrowi klasy wartość. Żeby sprawdzić jak to działa spróbujmy następującego przykładu:
class Klasa(object):
    def __setattr__(self, name, value):
        print 'Ustawiasz atrybut ' + str(name) + ' na ' + str(value)

if __name__ == '__main__':
    k = Klasa()
    k.a = 1
    k.imie = 'Jan'
    k.ja = 'ty'
    k.liczba = 123.4
Za każdym razem gdy ustawiamy jakiś atrybut wywoływana jest nasza funkcja __setattr__ :) Fantastyczne narzędzie :) Problem polega jednak na tym, że w momencie, w którym nadpisaliśmy tą funkcję musimy sami zadbać o przypisanie odpowiednich wartości. Kiedy poznawałem opisywany mechanizm próbowałem ponownie wywołać funkcję __setattr__ dla klasy albo przypisać go wewnątrz tradycyjną metodą poprzez znak równości. Kończyło się to rekurencyjnym zapętleniem się wywołań metody __setattr__ a to z kolei błędem.

Metodą proponowaną w dokumentacjidokumentacji, dla new-style classes, a tylko o takich piszę, jest wywołanie tej metody na obiekcie bazowym. Tak więc aby nasza klasa rzeczywiście zapamiętywała przypisywane atrybutom wartości nasza metoda __setattr__ powinna wyglądać tak:
class Klasa(object):
    def __setattr__(self, name, value):
        print 'Ustawiasz atrybut ' + str(name) + ' na ' + str(value)
        object.__setattr__(self, name, value)
i wszystko będzie działać jak powinno. Co nam daje ta metoda ? Pozwala nam tak naprawdę na kontrolę tego co i w jaki sposób jest przypisywane do naszej klasy nawet w przypadku, w gdy przypisywany jest parametr, którego nie zdefiniowaliśmy (sic!). Moglibyśmy dzięki tej metodzie sprawdzać czy w naszej klasie istnieje atrybut, któremu wartość chce nadać programista i wyrzucić wyjątek gdy taki nie jest zdefiniowany. Bardzo elastyczne narzędzie.

Czas na pobieranie atrybutów. Początkowo w języku Python istniała metoda __getattr__(self, name). Była wywoływana za każdym razem gdy odwoływaliśmy się do nie istniejącego w klasie atrybutu i chcieliśmy jakoś na to zareagować, np. zwrócić wyjątek lub coś innego. Pokaże na przykładzie jak to wygląda.
class Klasa(object):
    a = None
    def __getattr__(self, name):
        print 'Atrybutu ' + name + ' nie ma w naszej klasie.'

if __name__ == '__main__':
    k = Klasa()
    k.a
    k.b
Program wpisze komunikat "Atrybutu b nie ma w naszej klasie.". I bardzo dobrze. Kiedy odwołaliśmy się do atrybutu, który nie istniej Python skorzystał z metody __getattr__(self, name). Metoda nie została wywołana przy atrybucie a gdyż został wcześniej zdefiniowany.

Gdybyśmy chcieli zabezpieczyć naszą klasę przed przypisywaniem wartości nasza metoda __getattr__ powinna zgłaszać wyjątek raise AttributeError :) W ten sposób próba dopisania do obiektu czegoś czego nie zdefiniowaliśmy kończyła by się błędem (wyjątkiem).

Tak było w Pythonie przed wersją 2.2. W wraz z wejściem new-style classes postanowiono wprowadzić metodę __getattribute__(self, name), która w przeciwieństwie do __getattr__ zadziała zawsze (zarówno gdy atrybut, który chcemy pobrać istnieje jak również gdy nie istnieje). Zapożyczę przykład fofoo z jednego z wątków z forum PPCG w którym pytałem o dokładniejsze wyjaśnienie tego mechanizmu.
class C(object):
    x = None
    def __getattr__(self, name):
        print '__getattr__', name
        #raise AttributeError
    def __getattribute__(self, name):
        print '__getattribute__',name
        if name == 'whoops':
            raise AttributeError
        return object.__getattribute__(self, name)
    def metoda(self): pass

if __name__ == '__main__':
    c = C()
    c.x
    print
    c.metoda()
    print
    c.a
    print
    c.whoops
Metoda __getattribute__ wywoła się za każdym razem :) Ponieważ czasami atrybut istnieje i chcemy uzyskać do niego dostęp musimy na końcu zwrócić object.__getattribute__(self, name). W tym momencie Python da nam dostęp do elementu, który wskazaliśmy. Jeżeli jednak nie istnieje sterowanie zostanie przekazane do metody __getattr__. Metoda __getattr__ zostanie również wywołana, jeżeli w __getattribute__ zgłosimy wyjątek AttributeError :)

Aby dopełnić informacje na koniec należałoby dodać iż z metod takich jak __getattribute__ czy __getattr__ oraz __setattr__ do parametrów można dobierać się w bezpieczny sposób (nie powodujący rekurencyjnego wywołania tych funkcji) również poprzez słowniki. Dla przykładu.
class Klasa(object):
    a = 'a1'
    def __init__(self):
        self.b = 'b1'

if __name__ == '__main__':
    k = Klasa()
    print k.__dict__['b']
    print type(k).__dict__['a']
Nasza klasa posiada parametr klasowy a i parametr instancji b. Oba są trzymane w słownikach. Aby dobrać się do słownika konkretnego obiektu wystarczy użyć k.__dict__, dobranie się do słownika samej klasy wymaga użycia type(k).__dict__ :)

Informacje w dokumentacji dotyczące tematu oraz jego rozwinięcie znajdziecie pod adresami:
http://docs.python.org/reference/datamodel.html#customizing-attribute-access

No comments: