Showing posts with label parametry. Show all posts
Showing posts with label parametry. Show all posts

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

Monday, January 5, 2009

Python - funkcje o nieograniczonej liczbie parametrów

Tak chyba należałoby to zatytułować :) Cóż to ach cóż :]
Wyobraźmy sobie funkcję, która miałaby sumować wszystkie podane jej w parametrze liczby. Nie chcemy ograniczać się w ich ilości. Funkcja ma dodawać liczby do siebie i zwracać ich sumę. Czy podamy ich 3 czy 100 - ma działać :)

Pierwsze co przychodzi nam do głowy to może po prostu podawać krotkę parametrów. W sumie - czemu nie. W porządku, jednak to załóżmy iż nasz zmysł programistycznej estetyki cierpi na tym i nie daje nam spokoju. Czy funkcja w Pythonie może przyjmować nieskończenie parametrów.

Może :) "To super !" - ktoś powie. Super :) ale nie, jeżeli trzeba to tłumaczyć, bo w Pythonie całość można zrealizować na dwa sposoby. Ale pokolei.

Pierwsza metoda polega na podaniu w którejś kolejności jako parametr funkcji nazwy zaczynającej się od jednej gwiazdki (w naszym przykładzie będzie to jedyny parametr choć nie musi tak być). Na razie obejrzemy sobie tylko jak wygląda.
def suma(*args):
    print args

suma('ja', 3.4, (9 + 1j)) # ('ja', 3.4, (9 + 1j))
suma(1,2,3) # (1, 2, 3)

Co widzimy ? Otóż podczas gdy w liście parametrów funkcji nazwa jednego z argumentów jest poprzedzona gwiazdką (parametr ten nie musi nosić nazwy args jednak tak się w Pythonie przyjęło) możemy zajrzeć do niego uzyskując dostęp do wszystkich przekazanych do funkcji wartości. Okazuje się (jak widać), że args jest od "wnętrza" krotką, która przetrzymuje kolejno wszystkie przekazane wartości. Skoro już to wiemy :) stworzenie funkcji sumującej wszystkie przekazane do niej liczby jest banalne.
def suma(*args):
    wynik = 0
    for element in args:
        wynik += element
return wynik
Ponieważ args zawiera w sobie (krotka) wszystkie przekazane do funkcji parametry możemy przebiec się po nich pętlą for a następnie pododawać je do siebie :) i zwrócić wynik. Dzięki użyci w definicji *args możemy użyć dowolnej ilości elementów. Wszystkie znajdą się w naszej krotce i będziemy mogli je posumować :] Sprawdźmy działanie naszej funkcji:
suma(1,2,3)
suma(1,2,3,4,5,6,7,8,9,20)

nieważne ile parametrów podamy :) zadziała :]

No dobrze to jeden sposób :) Działa dobrze dla parametrów nienazwanych. Czasami na przykład chcielibyśmy mieć funkcję obsługującą "najróżniejsze parametry" i móc sprawdzić jakie nazwy im nadano w trakcie przekazywania ich. Gdy spróbujemy czegoś takiego suma(a=1,b=2,c=3) w naszej obecnej funkcji dostaniemy błąd. Wszystko dlatego, że za obsługę parametrów nazwanych odpowidzialna jest zmienna (argument) o nazwie poprzedzonej dwoma gwiazdkami ** zwyczajowo nazywana **kwargs.

Stwórzmy sobie funkcję test, która pokaże nam jak wygląda przekazywanie parametrów:
def test(*args, **kwargs):
    print args
    print kwargs
i uruchommy ją dla zobaczenia jak działa **kwargs test(1,2,3,d=4,e=5,f=6)
(1, 2, 3)
{'e': 5, 'd': 4, 'f': 6}


Jak widzimy :] Wszystkie parametry przekazane jako parametry nazwane występują pod zmienną kwargs (zmienna nazywana tak zwyczajowo jednak chodzi o tą poprzedzoną dwoma gwiazdkami). Jest to z oczywistych przyczyn (musi zawierać nazwę i wartość jej przypisaną) słownik :) Przy czym zawiera on wyłącznie parametry przekazane jako nazwane. Nadal wszystkie parametry przekazane jako nienazwane znajdziemy w *args.

To w sumie tyle :) Może powiem tylko o pewnej podstawowej rzeczy, o której należy pamiętać podczas wywoływania funkcji gdzie używamy parametrów nazwanych i nienazwanych. Wszystkie parametry nazwane muszą występować na końcu. Innymi słowami gdy wpiszesz już jako parametr nazwany wszystkie kolejne muszą być nazwanymi albo musi być to koniec parametrów :) Sprawdź sam !

Saturday, January 3, 2009

Python - parametry nazwane i nie nazwane

Weźmy na warsztat funkcję, która przyjmuje kilka parametrów:
def dane(imie="Jan", nazwisko="Kowalski", wiek=45):
print "Imię: " + imie
print "Nazwisko: " + nazwisko
print "Wiek: " + str(wiek)
Funkcja ma trzy parametry. Podajemy jej imię, nazwisko, wiek a jej zadaniem jest ich wyświetlenie w trzech kolejnych linijkach tych danych.

Nie interesuje nas wcale co ta funkcja robi (mogłaby robić kawę i nie miałoby to większego znaczenia), ale o parametry jakie przyjmuje :)
Zajmiemy się w tym newsie dokładnie tym jak my możemy je przekazywać :)

Funkcja posiada wartości domyślne dla wszystkich parametrów. Tak więc wiadomo, że wywołanie funkcji w postaci dane() wyświetli

Imię: Jan
Nazwisko: Kowalski
Wiek: 45


Przekazywanie parametrów, które przychodzi na myśl każdemu kto programował w czymś innym niż python to na przykład opcje (nie będę wypisywał wyjścia, tylko po komentarzu przyjęte wartości).


dane("Kazimierz") # Kazimierz, Kowalski, 45
dane("Zbigniew", "Kroten") # Zbigniew, Kroten, 45
dane("Marta", "Narlicka", 15) # Marta, Narlicka 15


I wszystko pięknie :) Gdy nie podamy jakiegoś parametru - podstawiana jest wartość podana w deklaracji funkcji (domyślna). Więc co jest takiego w Pythonie ? No właśnie czas na mały show.

Python obsługuje tak zwane parametry. Ponieważ nie mam do czego porównać powiem na czym polega. Otóż możesz podać nazwę parametru, który ustawiasz. Na przykład gdybyś chciał ustawić tylko wiek i imię (i nazwisko zostawić domyślne) możesz napisać:


dane(imie="Joanna", wiek=27) # Joanna, Kowalski, 27


Szalenie użyteczne jest to gdy chcemy ustawić tylko kilka konkretnych parametrów naszej funkcji (albo metody) omijając pozostałe. Dzięki temu nasze funkcje są bardziej elastyczne :) i ładniej wyglądają. Nazwy podane przed znakiem równości oznaczają te same nazwy, które podaliśmy w trakcie deklaracji funkcji :)
Gdy chcemy ustawić jego parametr podajemy po prostu: nazwa_parametr=wartosc. Ot cała filozofia :]

Dodam tylko, że w związku z całym zachwytem należy przeczytać o ograniczeniach związany z tym cudem :) nie mniej jest to przyjemna cecha Pythona :] Wstępnie Rubowcy mogą tylko pozazdrościć (na razie funkcjonalność ta jest tylko symulowana w Ruby na hashach).