La lezione è basata su https://realpython.com/primer-on-python-decorators/.
Un decoratore è una funzione che prende come argomento un'altra funzione e ne amplia il comportamento senza modificarla eplicitamente.
In Python le funzioni sono Oggetti di Prima Classe cioè possono essere passate come argomenti ad altre funzioni come qualunque altro oggetto (int, float, list etc.)
def say_hello(name):
return f"Hello {name}"
def be_awesome(name):
return f"Yo {name}, together we are the awesomest!"
def greet_bob(greeter_func, name ="Bob"):
return greeter_func(name)
greet_bob(say_hello) # function name as argument
'Hello Bob'
greet_bob(be_awesome)
'Yo Bob, together we are the awesomest!'
greet_bob(say_hello,"Peter") # function name and its input as arguments
'Hello Peter'
Possiamo definire funzioni all'interno di funzioni e una funzione può restituire funzioni:
def parent(num):
def first_child():
return "Hi, I am Emma"
def second_child():
return "Call me Liam"
if num == 1:
return first_child
else:
return second_child
parent
restituisce una referenza (indirizzo di memoria) a una funzione (Si noti che nel return
il nome della funzione è privo di parentesi tonde. Provate e restituire first_child()
), che possiamo poi invocare normalmente:
first = parent(1)
first
<function __main__.parent.<locals>.first_child()>
first()
'Hi, I am Emma'
def my_decorator(func): # decorator on a generic function with no input
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper # return reference to an internal function. `wrapper` non può essere invocato dall'esterno di `my_decorator`
wrapper()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-9-4cb166442f3c> in <module> ----> 1 wrapper() NameError: name 'wrapper' is not defined
Definiamo una funzione:
def say_whee():
print("Whee!")
Definiamo una versione decorata di say_whee
:
decorated_say_whee = my_decorator(say_whee) # a wrapper viene pasata la referenza alla funzione `say_whee`
decorated_say_whee() # decorated_say_whee viene effettivamente eseguita. `func` in wrapper ora punta a `say_whee`.
Something is happening before the function is called. Whee! Something is happening after the function is called.
print(say_whee)
print(decorated_say_whee)
<function say_whee at 0x107fb8af0> <function my_decorator.<locals>.wrapper at 0x107fb8940>
Possiamo anche ridefinire say_whee
come la versione decorata di se stessa:
say_whee = my_decorator(say_whee)
say_whee()
Something is happening before the function is called. Whee! Something is happening after the function is called.
print(say_whee)
<function my_decorator.<locals>.wrapper at 0x107fb8d30>
Riassumendo: un decoratore si avvolge
attorno ad una funzione e ne modifica il comportamento.
L'azione svolta da un decoratore può essere determinato dinamicamente, cioè quando la funzione decorata viene chiamata
from datetime import datetime
def not_during_the_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
else:
pass # Hush, the neighbors are asleep.
return wrapper
def honk():
print("Honk! Honk!")
honk = not_during_the_night(honk)
honk()
Honk! Honk!
Se chiamate honk
fra le 22 e le 7 non succede nulla.
Esiste un modo sintetico di ridefinire una funzione come la funzione stessa inglobata in un decoratore, usando il simbolo @
:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_whee():
print("Whee!")
@my_decorator
prima della definizione di say_whee
è equivalente a definire say_whee e poi aggiugere la linea
say_whee = my_decorator(say_whee)
.
Se i decoratori vengono salvati in un file possono essere importati come normali funzioni e utilizzati in ambiti diversi.
Esempio:
Supponiamo che in decoratori.py ci sia il codice che segue:
def do_twice(func):
def wrapper_do_twice():
func()
func()
return wrapper_do_twice
%cd ../ShellPrograms/
from decorators import do_twice
%cd ../ipynb
@do_twice # declare the decorated function
def say_whee_twice():
print("Whee!")
/Users/maina/cernbox/python/MyCourse2/ShellPrograms /Users/maina/cernbox/python/MyCourse2/ipynb
say_whee_twice() # call the decorated function
Whee! Whee!
Per decorare funzioni con input arbitrari è necessario utilizzare la notazione *args, **kwargs
. L'input è passato al wrapper interno che a sua volta lo passa alle funzioni da decorare.
def do_twice(func):
"""Run the decorated function twice"""
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice
@do_twice
def greet(name):
print(f"Hello {name}")
greet("Pippo")
Hello Pippo Hello Pippo
Nell'esempio seguente l'introduzione del decoratore functools.wraps serve a facilitare l'accesso alla funzione più interna
import functools
def do_twice(func):
"""Run the decorated function twice"""
@functools.wraps(func)
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
@do_twice
def return_greeting(name):
print("Creating greeting")
return f"Hi {name}"
return_greeting("Paperino")
Creating greeting Creating greeting
'Hi Paperino'
Verifichiamo che la funzione decorata ritorni effettivamente il risultato:
print("La funzione ha restituito:",return_greeting("Paperino"))
Creating greeting Creating greeting La funzione ha restituito: Hi Paperino
Python possiede dei decoratori built-in che vengono comunemente utilizzati nella costruzione di classi, @classmethod, @staticmethod, and @property. I decoratori @classmethod and @staticmethod vengono usati per definire metodi interni alla classe che non sono collegati alle particolari istanze della classe stessa. Il decoratore @property definisce un attributo (può essere richiamato senza le parentesi () ). In mancanza di un metodo setter
l'attributo non può essere modificato dall'esterno.
# I `@classmethods` sono essenzialmente degli inizializzatori più specializzati
# Notare che @classmethods ha come primo argomento il nome della classe e che viene ereditato dalle sottoclassi
# L'underscore in self._radius = radius vuol dire che radius è un attributo PRIVATO
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get value of radius"""
return self._radius
@radius.setter
def radius(self, value):
"""Set radius, raise error if negative"""
if value >= 0:
self._radius = value
else:
raise ValueError("Radius must be positive")
@property
def area(self): # area non ha setter
"""Calculate area inside circle"""
return self.pi() * self.radius**2
def cylinder_volume(self, height):
"""Calculate volume of cylinder with circle as base"""
return self.area * height
@classmethod
def unit_circle(cls):
"""Factory method creating a circle with radius 1"""
return cls(1)
@staticmethod
def pi():
"""Value of π (could use math.pi instead though, if math had been imported)"""
return 3.1415926536
C1 = Circle(3)
C1.radius
3
C1.area
28.2743338824
C1.radius = 3.4
C1.area
36.316811075615995
C1.area = 37.9
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-71-c14585f3be5c> in <module> ----> 1 C1.area = 37.9 AttributeError: can't set attribute
C2 = Circle(-1)
C2.radius = -2
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-77-2e4de9af6d7d> in <module> ----> 1 C2.radius = -2 <ipython-input-73-46ca95375c67> in radius(self, value) 20 self._radius = value 21 else: ---> 22 raise ValueError("Radius must be positive") 23 24 @property ValueError: Radius must be positive
Una sottoclasse di Circle:
class Cone(Circle):
def __init__(self, radius, height):
Circle.__init__(self, radius) # La parte di inizializzazione che riguarda `radius` viene eriditata da `Circle`
self._height = height
# La definizione dell'attributo radius e il setter corrispondente vengono eriditata da `Circle`
@property # Nuovo attributo specifico della sottoclasse
def height(self):
"""Get value of height"""
return self._height
@height.setter
def height(self, value):
"""Set radius, raise error if negative"""
if value >= 0:
self._height = value
else:
raise ValueError("height must be positive")
def cone_volume(self): # Il metodo utilizza un metodo della classe genitore
"""Calculate volume of cone with circle as base"""
return self.cylinder_volume(self.height)/3
cone1 = Cone(-1,3)
cone1.radius = -2
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-100-7afd1d5bd7fd> in <module> ----> 1 cone1.radius = -2 <ipython-input-73-46ca95375c67> in radius(self, value) 20 self._radius = value 21 else: ---> 22 raise ValueError("Radius must be positive") 23 24 @property ValueError: Radius must be positive
cone1.pi()
3.1415926536
cone2 = Cone(2,3)
cone2.cone_volume()
12.5663706144