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
'******************************************'