Dekoratory

Stronę tą wyświetlono już: 291 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:

Listing 1
  1. workers = [] # pracownicy
  2. coworkers = [] # współpracownicy
  3. clients = [] # klienci
  4. def add_interface(fu): # to jest funkcja dekorująca, przyjmuje ona jako argument etykietę funkcji, która będzie opakowywana
  5. def fu_add_interface(record = {}): # to jest deklaracja nowej funkcji, która opakuje starą
  6. if len(record) == 0:
  7. record = {'imię':input("Podaj dane\nImię: "), 'nazwisko':input("Nazwisko: ")}
  8. fu(record) # to jest wywołanie starej funkcji wewnątrz nowej
  9. return fu_add_interface # a tu zwracana jest etykieta utworzonej funkcji
  10. @add_interface # w ten sposób daje się do zrozumienia, że funkcja addWorker zostaje opakowana za pomocą funkcji add_interface
  11. def addWorker(record = {}):
  12. workers.append(record)
  13. @add_interface
  14. def addCoworker(record = {}):
  15. coworkers.append(record)
  16. @add_interface
  17. def addClient(record = {}):
  18. clients.append(record)
  19. def draw_interface(fu):
  20. def fu_draw_interface(records = {}):
  21. fu(records)
  22. for i in records:
  23. print("Imię: {i[imię]}, nazwisko: {i[nazwisko]}".format(i = i))
  24. return fu_draw_interface
  25. @draw_interface
  26. def drawWorkers(records):
  27. if len(records):
  28. print("Lista pracowników")
  29. @draw_interface
  30. def drawCoworkers(coworkers):
  31. if len(coworkers):
  32. print("Lista współpracowników")
  33. @draw_interface
  34. def drawClient(clients):
  35. if len(clients):
  36. print("Lista klientów")
  37. def draw():
  38. drawWorkers(workers)
  39. drawCoworkers(coworkers)
  40. drawClient(clients)
  41. def drawMenu():
  42. 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"]
  43. m = 0
  44. for i in commands:
  45. m = len(i) if len(i) > m else m
  46. print("{s}n{t}n{s}".format(s = "=" * (m + 4), t = "MENU GŁÓWNE".center(m + 4)))
  47. for i in zip(commands, range(len(commands))):
  48. print("{i[0]}{sp}[{i[1]}]".format(i = i, sp = " " * (m + 1 - len(i[0]))))
  49. return int(input("Wybierz, co chcesz zrobić: "))
  50. def menu():
  51. d = drawMenu()
  52. print("=" * 70)
  53. while d > 0:
  54. if d == 1:
  55. addWorker()
  56. elif d == 2:
  57. addCoworker()
  58. elif d == 3:
  59. addClient()
  60. elif d == 4:
  61. drawWorkers(workers)
  62. elif d == 5:
  63. drawCoworkers(coworkers)
  64. elif d == 6:
  65. drawClients(clients)
  66. elif d == 7:
  67. draw()
  68. elif d == 0:
  69. pass
  70. else:
  71. print("Niepoprawne polecenie:")
  72. if d > 0:
  73. d=drawMenu()
  74. print("=" * 70)
  75. menu()

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

Listing 2
  1. def add_interface(fu): # to jest funkcja dekorująca, przyjmuje ona jako argument etykietę funkcji, która będzie opakowywana
  2. def fu_add_interface(record = {}): # to jest deklaracja nowej funkcji, która opakuje starą
  3. if len(record) == 0:
  4. record = {'imię':input("Podaj danenImię: "), 'nazwisko':input("Nazwisko: ")}
  5. fu(record) # to jest wywołanie starej funkcji wewnątrz nowej
  6. return fu_add_interface # a tu zwracana jest etykieta utworzonej funkcji
  7. @add_interface # w ten sposób daje się do zrozumienia, że funkcja addWorker zostaje opakowana za pomocą funkcji add_interface
  8. def addWorker(record = {}):
  9. 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:

Listing 3
  1. @add_interface # w ten sposób daje się do zrozumienia, że funkcja addWorker zostaje opakowana za pomocą funkcji add_interface
  2. def addWorker(record = {}):
  3. 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:

Listing 4
  1. def addWorker(record = {}):
  2. workers.append(record)
  3. 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ą:

Listing 5
  1. class Point2D:
  2. def __init__(self, x = 0, y = 0):
  3. self._x = x
  4. self._y = y
  5. @property
  6. def x(self):
  7. return self._x
  8. @x.setter
  9. def x(self, x):
  10. self._x = x
  11. @property
  12. def y(self):
  13. return self._y
  14. @y.setter
  15. def y(self, y):
  16. self._y = y
  17. def __str__(self):
  18. return "Point2D(x={self._x}, y={self._y})".format(self = self)
  19. def draw(self):
  20. print(self)

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

Listing 6
  1. class Point2D:
  2. def __init__(self, x = 0, y = 0):
  3. self._x = x
  4. self._y = y
  5. def getx(self):
  6. return self._x
  7. def setx(self, x):
  8. self._x = x
  9. x = property(getx, setx, None, "I'm 'x' property")
  10. def gety(self):
  11. return self._y
  12. def sety(self, y):
  13. self._y = y
  14. y=property(gety, sety, None, "I'm 'y' property")
  15. def __str__(self):
  16. return "Point2D(x={self._x}, y={self._y})".format(self = self)
  17. def draw(self):
  18. print(self)

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

Listing 7
  1. p = Point2D(1,2)
  2. p.draw()
  3. p.x = 10
  4. p.y = 20
  5. p.draw()
  6. 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:

Listing 8
  1. class Point2D:
  2. _x = 0
  3. _y = 0
  4. def __init__(self, x = 0, y = 0):
  5. Point2D._x = x
  6. Point2D._y = y
  7. self._x = x
  8. self._y = y
  9. @property
  10. def x(self):
  11. return self._x
  12. @x.setter
  13. def x(self, x):
  14. self._x = x
  15. @property
  16. def y(self):
  17. return self._y
  18. @y.setter
  19. def y(self, y):
  20. self._y = y
  21. def __str__(self):
  22. return "Point2D(x={self._x}, y={self._y})".format(self = self)
  23. def draw(self):
  24. Point2D._x = self.x
  25. Point2D._y = self.y
  26. print(self)
  27. @classmethod
  28. def lastPoint(cls):
  29. 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:

Listing 9
  1. Point2D.lastPoint()
  2. p = Point2D(10,20)
  3. p.lastPoint()
  4. Point2D.lastPoint()
  5. p.x = 5
  6. p.y = 4
  7. p.lastPoint()
  8. Point2D.lastPoint()
  9. p.draw()
  10. 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:

Listing 10
  1. class Point2D:
  2. _x = 0
  3. _y = 0
  4. def __init__(self, x = 0, y = 0):
  5. Point2D._x = x
  6. Point2D._y = y
  7. self._x = x
  8. self._y = y
  9. @property
  10. def x(self):
  11. return self._x
  12. @x.setter
  13. def x(self, x):
  14. self._x = x
  15. @property
  16. def y(self):
  17. return self._y
  18. @y.setter
  19. def y(self, y):
  20. self._y = y
  21. def __str__(self):
  22. return "Point2D(x={self._x}, y={self._y})".format(self = self)
  23. def draw(self):
  24. Point2D._x = self.x
  25. Point2D._y = self.y
  26. print(self)
  27. @classmethod
  28. def lastPoint(cls):
  29. print(Point2D(cls._x, cls._y))
  30. @staticmethod
  31. def i_ver():
  32. return Point2D(1, 0)
  33. @staticmethod
  34. def j_ver():
  35. 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:

Listing 11
  1. iver = Point2D.i_ver()
  2. iver.draw()

Wynik działania:

Point2D(x=1, y=0)

Komentarze

Lakus

Data: 19-11-2016 00:22:45

Kurs Pythona jest nieźle zrobiony, jednak te trudniejsze części (klasy, dekoratory) wymagają szerszego omówienia. Nie wszystko jest tutaj zrozumiałe.