Dekoratory funkcji i klas#
Domknięcie (closure)#
Domknięcie, w metodach realizacji języków programowania, jest to obiekt wiążący funkcję oraz środowisko, w jakim ta funkcja ma działać. Środowisko przechowuje wszystkie obiekty wykorzystywane przez funkcję, niedostępne w globalnym zakresie widoczności. Realizacja domknięcia jest zdeterminowana przez język, jak również przez kompilator.
Domknięcia występują głównie w językach funkcyjnych, w których funkcje mogą zwracać inne funkcje, wykorzystujące zmienne utworzone lokalnie.
def bind_add(x):
def add(y):
# x jest "zamknięte" w definicji
return y + x
return add
add_5 = bind_add(5)
add_5(10)
15
add_665 = bind_add(665)
add_665(2)
667
Wprowadzenie do dekoratorów#
Dekorator to wzorzec projektowy, pozwalający na dynamiczne dodanie nowej funkcjonalności, w trakcie działania programu.
W języku Python jest to metoda modyfikacji obiektu wywoływalnego (funkcji, metod klasy, klas) za pomocą domknięć.
Dekoratory są w Pythonie często spotykaną techniką programistyczną. Ich zalety to redukcja ilości kodu oraz możliwość kontrolowania funkcji (lub innych obiektów wywoływalnych), w szczególności ich danych wejściowych i zwracanych wartości.
Prosty dekorator#
Poniżej przedstawiono implementację dekoratora @shouter
.
Funkcje udekorowane nim wyświetlają komunikat na początku i pod koniec ich wywołania.
def shouter(func):
def wrapper():
print("Before", func.__name__)
result = func()
print(result)
print("After", func.__name__)
return result
return wrapper
Można tak zdefiniowanej funkcji użyć do “nadpisania” istniejącej już funkcji (tak naprawdę do zmiany tego, na co wskazuje zmienna):
def greetings():
return "Hi"
hello = shouter(greetings)
hello()
Before greetings
Hi
After greetings
'Hi'
Począwszy od Pythona 2.4, możliwe i rekomendowane jest użycie specjalnej składni:
@shouter
def hello():
return "Hello"
hello()
Before hello
Hello
After hello
'Hello'
Użycie @shouter def hello()
jest równoważne hello = shouter(hello)
.
Argumenty w dekoratorach#
Problem
Przedstawiony dekorator działa tylko z funkcjami, które nie przyjmują żadnych argumentów. Co z funkcjami wymagającymi argumentów?
@shouter
def add(x, y):
'''Docstring for add(x, y)'''
return x + y
add(2, 7)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[9], line 1
----> 1 add(2, 7)
TypeError: shouter.<locals>.wrapper() takes 0 positional arguments but 2 were given
Innym problemem jest to, że udekorowana funkcja utraciła swój docstring oraz swoją nazwę:
add.__doc__
add.__name__
'wrapper'
Rozwiązanie
Argumenty przekazywane do wrapper muszą zostać przekazane dalej, do właściwej funkcji func.
Z kolei problem z docstringiem i nazwą rozwiążemy dekorując funkcję wrapper przy pomocy dekoratora @functools.wraps
, który zadba o skopiowanie docstringa i nazwy:
import functools
def shouter(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before", func.__name__)
result = func(*args, **kwargs)
print(result)
print("After", func.__name__)
return result
return wrapper
@shouter
def add(x, y):
'''Docstring for add(x, y)'''
return x + y
add(5, 6)
Before add
11
After add
11
add.__doc__
'Docstring for add(x, y)'
add.__name__
'add'
Dekoratory parametryzowane#
Dekoratory, które nie przyjmują żadnych argumentów, są często spotykane. Jednak czasami potrzebujemy przekazać do dekoratora argumenty.
Aby otrzymać parametryzowany dekorator, musimy go “owinąć” w jeszcze jedną funkcję (domknięcie):
def tag(tagname):
def decorator(fun):
@functools.wraps(fun)
def wrapper(*args, **kwargs):
tag_before = f"<{tagname}>"
tag_after = f"</{tagname}>"
fresult= fun(*args, **kwargs)
return tag_before + fresult + tag_after
return wrapper
return decorator
@tag("b")
def output(data):
return data
output("TEXT")
'<b>TEXT</b>'
Użycie @tag("b")
jest odpowiednikiem:
output = tag("b")(output)
Wiele dekoratorów#
Funkcję można owijać w wiele dekoratorów.
@shouter
@tag('b')
def my_func(text):
return text
my_func("text")
Before my_func
<b>text</b>
After my_func
'<b>text</b>'
Należy pamiętać, że kolejność ma znaczenie.
Składnia @shouter @tag("b") def my_func()
jest równoważna my_func shouter(tag("b")(my_function)
Kiedy uruchamiane są dekoratory#
Kluczowe znaczenie dla dekoratorów ma fakt, że są one uruchamiane zaraz po tym jak zdefiniowana została dekorowana funkcja. Najczęściej jest to moment importu pakietu.
registry = []
def register(func):
print(f'running register({func})')
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
running register(<function f1 at 0x7f4677b749d0>)
running register(<function f2 at 0x7f4677b74790>)
running main()
registry -> [<function f1 at 0x7f4677b749d0>, <function f2 at 0x7f4677b74790>]
running f1()
running f2()
running f3()
Dekoratory klas#
Od Pythona 2.6 można dekorować klasy. W środku dekoratora można zmodyfikować klasę, na przykład zmienić jej metody. Dekoratory klas mają działanie zbliżone do metaklas.
id = 0
def add_id(decorated_class):
original_init = decorated_class.__init__
def __init__(self, *args, **kwargs):
print("add_id init")
global id
id += 1
self.id = id
original_init(self, *args, **kwargs)
decorated_class.__init__ = __init__ # replacing __init__ in decorated class
return decorated_class
@add_id
class Foo(object):
def __init__(self):
print("Foo class init")
foo = Foo()
foo.id
add_id init
Foo class init
1
bar = Foo()
bar.id
add_id init
Foo class init
2
Klasy jako dekoratory#
Bardzo ciekawym zastosowaniem jest użycie klasy jako dekoratora.
Wystarczy zdefiniować w klasie metodę specjalną __call__
.
Instancja klasy (uzyskana za pomocą operatora ()
) staje się wtedy obiektem, który można wywołać.
Jest to alternatywa dla definiowania nieparametryzowanego dekoratora przy pomocy dwóch zagnieżdżonych funkcji. Kod jest nieco prostszy do zrozumienia:
import functools
class Shouter:
def __init__(self, function):
print("Inside decorator's __init__()")
self.function = function
functools.update_wrapper(self, function)
def __call__(self, *args, **kwargs):
print("Before call")
result = self.function(*args, **kwargs)
print("After call")
return result
@Shouter
def anwser(input):
print("Inside function()")
return input * 42
Inside decorator's __init__()
anwser('*')
Before call
Inside function()
After call
'******************************************'