Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 5899 razy

Po co są i czym są dekoratory

Zdarza się czasami, że istnieje konieczność stworzenia pewnego zbioru funkcji, które z kolei będą miały (po części) taki sam kod. I teraz jest problem, bo zauważmy, że jeżeli powtarzam ten sam kod programu w wielu miejscach pisząc różne funkcje, to de facto tracę cenny czas na pisanie tych samych rzeczy oraz jeżeli dojdzie do pomyłki, albo jeśli zajdzie potrzeba modyfikacji tej części kodu to mam do poprawienia ten sam kod w wielu miejscach programu. Można by to rozwiązać tworząc pewną dodatkową funkcję, która by wykonywała ten kod za mnie i wstawiać go w miejscu, gdzie powinien się znaleźć. Jednakże bardziej eleganckim i typowo Pythonowym rozwiązaniem jest użycie dekoratora. Żeby dużo nie pisać, oto prosty przykład użycia dekoratorów:

workers = [] # pracownicy coworkers = [] # współpracownicy clients = [] # klienci def add_interface(fu): # to jest funkcja dekorująca, przyjmuje ona jako argument etykietę funkcji, która będzie opakowywana def fu_add_interface(record = {}): # to jest deklaracja nowej funkcji, która opakuje starą if len(record) == 0: record = {'imię':input("Podaj dane\nImię: "), 'nazwisko':input("Nazwisko: ")} fu(record) # to jest wywołanie starej funkcji wewnątrz nowej return fu_add_interface # a tu zwracana jest etykieta utworzonej funkcji @add_interface # w ten sposób daje się do zrozumienia, że funkcja addWorker zostaje opakowana za pomocą funkcji add_interface def addWorker(record = {}): workers.append(record) @add_interface def addCoworker(record = {}): coworkers.append(record) @add_interface def addClient(record = {}): clients.append(record) def draw_interface(fu): def fu_draw_interface(records = {}): fu(records) for i in records: print("Imię: {i[imię]}, nazwisko: {i[nazwisko]}".format(i = i)) return fu_draw_interface @draw_interface def drawWorkers(records): if len(records): print("Lista pracowników") @draw_interface def drawCoworkers(coworkers): if len(coworkers): print("Lista współpracowników") @draw_interface def drawClient(clients): if len(clients): print("Lista klientów") def draw(): drawWorkers(workers) drawCoworkers(coworkers) drawClient(clients) def drawMenu(): commands = ["Wyjście z programu", "Dodanie pracownika", "Dodanie współpracownika", "Dodanie klienta", "Wyświetlenie pracowników", "Wyświetlenie współpracowników", "Wyświetlenie klientów","Wyświetl wszyskich"] m = 0 for i in commands: m = len(i) if len(i) > m else m print("{s}n{t}n{s}".format(s = "=" * (m + 4), t = "MENU GŁÓWNE".center(m + 4))) for i in zip(commands, range(len(commands))): print("{i[0]}{sp}[{i[1]}]".format(i = i, sp = " " * (m + 1 - len(i[0])))) return int(input("Wybierz, co chcesz zrobić: ")) def menu(): d = drawMenu() print("=" * 70) while d > 0: if d == 1: addWorker() elif d == 2: addCoworker() elif d == 3: addClient() elif d == 4: drawWorkers(workers) elif d == 5: drawCoworkers(coworkers) elif d == 6: drawClients(clients) elif d == 7: draw() elif d == 0: pass else: print("Niepoprawne polecenie:") if d > 0: d=drawMenu() print("=" * 70) menu()

Przyjrzyjmy się nieco dokładnie następującemu fragmentowi kodu:

def add_interface(fu): # to jest funkcja dekorująca, przyjmuje ona jako argument etykietę funkcji, która będzie opakowywana def fu_add_interface(record = {}): # to jest deklaracja nowej funkcji, która opakuje starą if len(record) == 0: record = {'imię':input("Podaj danenImię: "), 'nazwisko':input("Nazwisko: ")} fu(record) # to jest wywołanie starej funkcji wewnątrz nowej return fu_add_interface # a tu zwracana jest etykieta utworzonej funkcji @add_interface # w ten sposób daje się do zrozumienia, że funkcja addWorker zostaje opakowana za pomocą funkcji add_interface def addWorker(record = {}): workers.append(record)

Utworzona funkcja add_interface jest tak zwanym dekoratorem. Dekorator przyjmuje tylko jeden argument, którym jest etykieta funkcji, która ma być opakowana. Wewnątrz funkcji dekorującej add_interface znajduje się definicja drugiej funkcji fu_add_interface, która z kolei musi przyjmować te same argumenty co funkcja dekorowana. Zauważyć należy, że wewnątrz definicji funkcji fu_add_interface wykonywany jest pewien kod a następnie wywoływana jest funkcja fu podana jako argument dekoratora.

W Pythonie został utworzony specjalny mechanizm do dekorowania danej funkcji. Fragment kodu z tym związany poniżej zamieszczam:

@add_interface # w ten sposób daje się do zrozumienia, że funkcja addWorker zostaje opakowana za pomocą funkcji add_interface def addWorker(record = {}): workers.append(record)

Zapis @add_interface umieszczony przed definicją funkcji addWorker oznacza, że ta funkcja zostanie opakowana przez funkcję dekorującą add_interface. Mechanizm ten można zapisać w nieco bardziej zrozumiały sposób:

def addWorker(record = {}): workers.append(record) addWorker = add_interface(addWorker)

Efekt działania powyższego kodu jest równoważny z wcześniej omawianym fragmentem. W praktyce stosuje się ten pierwszy, bo krótszy zapis dekoratora.

Standardowe dekoratory metod klas

Klasy mają swoje standardowe zbiory dekoratorów, które są dość często wykorzystywane przez Pythonautów. W tej części postaram się przybliżyć te najczęściej wykorzystywane.

Dekorator @property i @setter

Pola klasy w Pythonie nie mogą być oznaczona jako prywatne, czy chronione. Dzieje się tak dlatego, że Pythonauci nie bardzo uznają tego typu rozwiązanie za słuszne. Innymi słowy dostęp do pól klasy jest, był i będzie otwarty z każdego miejsca w programie, ale można sprawić, że w podpowiedzi kontekstowej nie będą się pojawiały nazwy pól klasy. Dzieje się tak dla takich pól klasy, które mają nazwę zaczynającą się od dolnej spacji. Utwórzmy sobie małą klasę przykładową:

class Point2D: def __init__(self, x = 0, y = 0): self._x = x self._y = y @property def x(self): return self._x @x.setter def x(self, x): self._x = x @property def y(self): return self._y @y.setter def y(self, y): self._y = y def __str__(self): return "Point2D(x={self._x}, y={self._y})".format(self = self) def draw(self): print(self)

Istnieje jeszcze jeden sposób użycia tego samego dekoratora:

class Point2D: def __init__(self, x = 0, y = 0): self._x = x self._y = y def getx(self): return self._x def setx(self, x): self._x = x x = property(getx, setx, None, "I'm 'x' property") def gety(self): return self._y def sety(self, y): self._y = y y=property(gety, sety, None, "I'm 'y' property") def __str__(self): return "Point2D(x={self._x}, y={self._y})".format(self = self) def draw(self): print(self)

Dekorator @property jest klasą. Dzięki użyciu tego dekoratora można zrobić teraz coś takiego:

p = Point2D(1,2) p.draw() p.x = 10 p.y = 20 p.draw() print("x={p.x}; y={p.y}".format(p=p))

Wynik działania powyższego kodu:

Point2D(x=1, y=2)
Point2D(x=10, y=20)
x=10; y=20

Dekorator @classmethod

Dekorator @classmethod umożliwia utworzenie metody, która nie będzie miała dostępu do pól obiektu klasy, ale będzie miała dostęp do atrybutów klasy. Stwórzmy sobie taki oto przykład:

class Point2D: _x = 0 _y = 0 def __init__(self, x = 0, y = 0): Point2D._x = x Point2D._y = y self._x = x self._y = y @property def x(self): return self._x @x.setter def x(self, x): self._x = x @property def y(self): return self._y @y.setter def y(self, y): self._y = y def __str__(self): return "Point2D(x={self._x}, y={self._y})".format(self = self) def draw(self): Point2D._x = self.x Point2D._y = self.y print(self) @classmethod def lastPoint(cls): print(Point2D(cls._x, cls._y))

W powyższym kodzie metoda klasy lastPoint wypisuje współrzędne punktu ostatnio utworzonego lub wyświetlonego za pomocą metody draw. Dla lepszego zrozumienia przetestujmy sobie taki oto kod:

Point2D.lastPoint() p = Point2D(10,20) p.lastPoint() Point2D.lastPoint() p.x = 5 p.y = 4 p.lastPoint() Point2D.lastPoint() p.draw() Point2D.lastPoint()

Wynik będzie następujący:

Point2D(x=0, y=0)
Point2D(x=10, y=20)
Point2D(x=10, y=20)
Point2D(x=10, y=20)
Point2D(x=10, y=20)
Point2D(x=5, y=4)

Dekorator @staticmethod

Dekorator @staticmethod umożliwia tworzenie metod, które nie są powiązane bezpośrednio z żadnym obiektem funkcji, ani też z klasą, w której definicja tejże metody się znajduje. Takie metody przypominają statyczne metody z C++ czy też innych języków programowania. Oto przykład:

class Point2D: _x = 0 _y = 0 def __init__(self, x = 0, y = 0): Point2D._x = x Point2D._y = y self._x = x self._y = y @property def x(self): return self._x @x.setter def x(self, x): self._x = x @property def y(self): return self._y @y.setter def y(self, y): self._y = y def __str__(self): return "Point2D(x={self._x}, y={self._y})".format(self = self) def draw(self): Point2D._x = self.x Point2D._y = self.y print(self) @classmethod def lastPoint(cls): print(Point2D(cls._x, cls._y)) @staticmethod def i_ver(): return Point2D(1, 0) @staticmethod def j_ver(): return Point2D(0,1)

Metody statyczne i_ver oraz j_ver mają za zadanie utworzyć i zwrócić obiekt, który jest wersorem: pierwszy - osi x; drugi - osi y. Oto przykładowy kod:

iver = Point2D.i_ver() iver.draw()

Wynik działania:

Point2D(x=1, y=0)