A dekorátorok olyan függvények, amelyek egy másik függvényt vesznek át argumentumként azzal a céllal, hogy kiterjesszék vagy lecseréljék az átadott függvény viselkedését.
Ha az A függvény egy dekorátor, a B függvény pedig az argumentuma, akkor az A függvény módosítja, kiterjeszti vagy lecseréli a B függvény viselkedését anélkül, hogy a B függvény kódját módosítaná.
Úgy mondjuk, hogy az A dekorátor függvény becsomagolja a B függvényt.
Bár a viselkedés „módosításáról” beszélünk, a becsomagolt függvény valójában nem változik meg.
A viselkedés vagy a becsomagolt függvény köré épül be (és az köré is, amit a függvény visszaad), vagy a becsomagolt függvény viselkedésének helyébe egy másik viselkedés lép.
A magasabb rendű függvény olyan függvény, amely egy vagy több függvényt fogad el argumentumként, és/vagy egy vagy több függvényt ad vissza. Az a függvény, amelyet argumentumként használunk, vagy amelyet egy másik függvény ad vissza, első osztályú függvény. A Python-függvény hívható objektum, ezért első osztályú függvény: eltárolható egy változóban, vagy argumentumként használható, akárcsak bármely más érték vagy objektum. A magasabb rendű függvények és az első osztályú függvények együtt teszik lehetővé a dekorátorokat.
A @ szimbólumot a dekorátorfüggvény neve elé írjuk, közvetlenül a dekorálni kívánt függvény fölé, így:
@decorator
def decorated_function():
pass
Egyes dekorátorok argumentumot fogadnak:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
Ha egy dekorátorhoz alapértelmezett argumentumokat definiáltál, akkor a @decorator() hívásban kötelező kiírni a zárójelet, hogy a dekorátor működjön, akárcsak bármely más függvény hívásakor:
@decorator_with_default_arg()
def decorated_function3():
pass
Ha a dekorátor pozicionális argumentumokat vár, és nem adod meg őket, hibaüzenetet kapsz, valahogy így:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
A @decorator szintaxis szintaktikai cukor, vagyis rövidítés arra, hogy meghívjuk a dekoráló függvényt, és argumentumként átadjuk neki a dekorált függvényt.
Az alábbiakban példákat látsz arra, hogyan hívhatod meg a dekorátort más formában:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
A legtöbb dekorátor célja, hogy kiterjessze vagy lecserélje egy másik függvény viselkedését, de előfordulhat, hogy egy dekorátor nem tesz mást, csak visszaadja a becsomagolt függvényt.
A dekorátorok olyan függvények, amelyek legalább egy argumentumot kapnak: azt a függvényt, amelyet becsomagolnak. Általában vagy a becsomagolt függvényt adják vissza, vagy egy olyan kifejezés eredményét, amely a becsomagolt függvényt használja.
Egy egyszerű dekorátor, amely pusztán visszaadja a becsomagolt függvényt, így írható meg:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
Egy dekorátor csak mellékhatásokat is adhat hozzá, például naplózáshoz használt kiegészítő információkat:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
A dekorátor nem önmagát adja vissza. Visszaadhatja a függvényargumentumait, egy másik függvényt, vagy egy vagy több értéket, amelyek az átadott, azaz a dekorált függvény visszatérési értékének helyébe lépnek. Ha a dekorátor egy másik függvényt ad vissza, az általában egy belső függvény.
Egy függvényt definiálhatsz egy másik függvényen belül is. Az ilyen egymásba ágyazott függvényt belső függvénynek nevezzük. A dekorátor belső függvénnyel is becsomagolhatja a függvényargumentumát. A dekorátor ezután a belső függvényét adja vissza. A belső függvény ezután visszaadhatja az eredeti függvényargumentumot.
Íme egy példa egy ellenőrzésre használt dekorátorra:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
Az első sorban a dekorátor func argumentumának definíciója áll.
A következő sorban a dekorátor belső függvényének a definíciója áll, amely becsomagolja a func argumentumot.
Mivel a belső függvény csomagolja a dekorátor func argumentumát, ugyanazt az argumentumot kapja, mint amit a func kap.
Figyeld meg, hogy a wrapper függvény nem köteles ugyanazt a nevet használni az argumentumhoz, mint amit a func definiált.
Az eredeti függvény a planet, a dekorátor pedig a world nevet használja, és a dekorátor így is működik.
A belső függvény vagy visszaadja a func-ot, vagy ha world == "Pluto", akkor kiírja, hogy a Plútó nem bolygó.
Ehelyett úgy is megírhatnánk, hogy ValueError-t dobjon.
Tehát a belső függvény becsomagolja a func-ot, és vagy visszaadja a func-ot, vagy olyasmit tesz, ami a func helyébe lép.
A dekorátor a belső függvényét adja vissza.
A belső függvény vagy visszaadja az eredetileg átadott függvényt, vagy nem.
Attól függően, hogy milyen kód fut le feltételesen a wrapper függvényben vagy a belső függvényben, a func visszaadásra kerülhet, hiba dobódhat, vagy a func visszatérési típusához tartozó érték adódhat vissza.
Olyan függvényekhez is írhatsz dekorátort, amelyek tetszőleges számú argumentumot fogadnak, ehhez a *args és **kwargs szintaxist használhatod.
Íme egy példa egy olyan függvény dekorátorára, amely tetszőleges számú argumentumot fogad:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
Ez akkor működik, ha meg akarjuk duplázni a függvényargumentum visszatérési értékét. Ha meg akarjuk háromszorozni, megnégyszerezni stb. a visszatérési értéket, akkor magához a dekorátorhoz adhatunk egy paramétert, ahogy azt a következő szakaszban bemutatjuk.
Íme egy példa egy olyan dekorátorra, amely beállítható úgy, hogy a dekorált függvény visszatérési értékét tetszőleges számmal megszorozza:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
Az első sorok ellenőrzik, hogy a factor nem 0.
Ezután következik az outer wrapper definíciója.
Ennek ugyanaz a szignatúrája, mint amit egy paraméter nélküli dekorátortól elvárunk.
Az outer wrappernek van egy belső függvénye, amelynek a szignatúrája megegyezik az eredeti függvényével.
Az inner wrapper végzi el a munkát: az eredeti függvény visszatérési értékét megszorozza a dekorátornak átadott argumentummal.
Az outer wrapper visszaadja az inner wrappert, a dekorátor pedig az outer wrappert adja vissza.
Íme egy példa egy paraméterezett dekorátorra, amely szabályozza, hogy ellenőrzi-e az eredeti függvénynek átadott argumentumot:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
Így könnyen átválthatsz arra, hogy ellenőrizze a Pluto értéket, vagy ne, méghozzá anélkül, hogy módosítanod kellene a my_func függvényt.