## Python Decorators --- ### Definition <p style='text-align: justify; font-size: 20pt'> A decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it </p> ```python @uppercase def greet(): return 'Hello!' ``` ```python >>> greet() 'HELLO!' ``` <p style='text-align: justify; font-size: 20pt'> Decorators provide a simple syntax for calling higher-order functions. </p> --- ### Functions are objects ```python def yell(text): return text.upper() + '!' ``` ```python >>> yell('hello') HELLO! ``` ```python >>> bark = yell ``` ```python >>> bark('woof') WOOF! ``` ```python >>> del yell >>> yell('hello?') NameError: "name 'yell' is not defined" >>> bark('hey') 'HEY!' ``` ```python >>> bark.__name__ 'yell' --- #### Functions can be stored in data structures ```python >>> funcs = [bark, str.lower, str.capitalize] >>> funcs [<function yell at 0x10ff96510>, <method 'lower' of 'str' objects>, <method 'capitalize' of 'str' objects>] ``` ```python >>> for f in funcs: ... print(f, f('hey there')) <function yell at 0x10ff96510> 'HEY THERE!' <method 'lower' of 'str' objects> 'hey there' <method 'capitalize' of 'str' objects> 'Hey there' ``` <p style='text-align: justify; font-size: 20pt'> You can even call a function object stored in the list without first assigning it to a variable: </p> ```python >>> funcs[0]('heyho') 'HEYHO!' ``` --- #### Functions can be passed to other functions: ```python def greet(func): greeting = func('Hi, I am a Python program') print(greeting) ``` <p style='text-align: justify; font-size: 20pt'> You can influence the resulting greeting by passing in different functions: </p> ```python >>> greet(bark) 'HI, I AM A PYTHON PROGRAM!' ``` --- ### Inner functions ```python def speak(text): def whisper(t): return t.lower() + '...' return whisper(text) ``` ```sh >>> speak('Hello, World') 'hello, world...' ``` <br> ```python >>> whisper('Yo') # ??? ``` --- ### Inner functions ```python def speak(text): def whisper(t): return t.lower() + '...' return whisper(text) ``` ```sh >>> speak('Hello, World') 'hello, world...' ``` <br> ```python >>> whisper('Yo') NameError: "name 'whisper' is not defined" ``` --- ### Inner functions ```python def speak(text): def whisper(t): return t.lower() + '...' return whisper(text) ``` ```sh >>> speak('Hello, World') 'hello, world...' ``` <br> ```python >>> whisper('Yo') NameError: "name 'whisper' is not defined" >>> speak.whisper # ??? ``` --- ### Inner functions ```python def speak(text): def whisper(t): return t.lower() + '...' return whisper(text) ``` ```sh >>> speak('Hello, World') 'hello, world...' ``` <p style='text-align: justify; font-size: 20pt'> But inner function 'whisper' does not exist outside 'speak': </p> ```python >>> whisper('Yo') NameError: "name 'whisper' is not defined" >>> speak.whisper AttributeError: "'function' object has no attribute 'whisper'" ``` --- ### Return functions <p style='text-align: justify; font-size: 20pt'> Functions are objects — you can return the inner function to the caller of the parent function: </p> ```python def get_speak_func(volume): def whisper(text): return text.lower() + '...' def yell(text): return text.upper() + '!' if volume > 0.5: return yell else: return whisper ``` --- <p style='text-align: justify; font-size: 20pt'> get_speak_func doesn’t call any of its inner functions - it simply returns the function object: </p> ```python >>> get_speak_func(0.3) <function get_speak_func.<locals>.whisper at 0x10ae18> >>> get_speak_func(0.7) <function get_speak_func.<locals>.yell at 0x1008c8> ``` <p style='text-align: justify; font-size: 20pt'> You can call the returned function: </p> ```python >>> speak_func = get_speak_func(0.7) >>> speak_func('Hello') 'HELLO!' ``` --- #### Functions can capture local state ```python def make_adder(n): def add(x): return x + n return add ``` ```python >>> plus_3 = make_adder(3) >>> plus_5 = make_adder(5) >>> plus_3(4) 7 >>> plus_5(4) 9 ``` Functions that do this are called lexical closures. --- ### Simple Decorators ```python def my_decorator(func): def wrapper(): print("Do something before the function is called.") func() print("Do something after the function is called.") return wrapper ``` ```python def say_whee(): print("Whee!") ``` ```python say_whee = my_decorator(say_whee) ``` ```sh >>> say_whee() Do something before the function is called. Whee! Do something after the function is called. ``` <p style='text-align: justify; font-size: 20pt'> The name say_whee now points to the wrapper() inner function: </p> ```python >>> say_whee <function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0> ``` --- ### Syntactic Sugar <p style='text-align: justify; font-size: 20pt'> Using the @ syntax is just syntactic sugar and a shortcut for this commonly used pattern. </p> ```python @my_decorator def say_whee(): print("Whee!") ``` <p style='text-align: justify; font-size: 20pt'> @my_decorator is just an easier way of saying:</p> <p style='text-align: center; font-size: 20pt'>say_whee = my_decorator(say_whee)</p> <p style='text-align: justify; font-size: 20pt'> It’s how you apply a decorator to a function. </p> --- #### Applying multiple decorators to a function ```python def strong(func): def wrapper(): return '<strong>' + func() + '</strong>' return wrapper def emphasis(func): def wrapper(): return '<em>' + func() + '</em>' return wrapper ``` ```python @strong @emphasis def greet(): return 'Hello!' ``` ```python >>> greet() # ??? ``` --- #### Applying multiple decorators to a function ```python def strong(func): def wrapper(): return '<strong>' + func() + '</strong>' return wrapper def emphasis(func): def wrapper(): return '<em>' + func() + '</em>' return wrapper ``` ```python @strong @emphasis def greet(): return 'Hello!' ``` ```python >>> greet() '<strong><em>Hello!</em></strong>' ``` --- #### Applying multiple decorators to a function ```python def strong(func): def wrapper(): return '<strong>' + func() + '</strong>' return wrapper def emphasis(func): def wrapper(): return '<em>' + func() + '</em>' return wrapper ``` ```python @strong @emphasis def greet(): return 'Hello!' ``` ```python >>> greet() '<strong><em>Hello!</em></strong>' ``` <p style='text-align: justify; font-size: 20pt'> The decorators were applied from bottom to top. The chain of decorator function calls looks like this: </p> ```python decorated_greet = strong(emphasis(greet)) ``` --- #### Decorating functions with arguments ```python def do_twice(func): def wrapper_do_twice(): func() func() return wrapper_do_twice ``` ```python @do_twice def greet(name): print(f"Hello {name}") ``` ```python >>> greet("World") # ??? ``` --- #### Decorating functions with arguments ```python def do_twice(func): def wrapper_do_twice(): func() func() return wrapper_do_twice ``` ```python @do_twice def greet(name): print(f"Hello {name}") ``` ```python >>> greet("World") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given ``` --- #### Decorating functions with arguments <p style='text-align: justify; font-size: 20pt'> The solution is to use *args and **kwargs in the inner wrapper function. Then it will accept an arbitrary number of positional and keyword arguments: </p> ```python def do_twice(func): def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) func(*args, **kwargs) return wrapper_do_twice ``` ```python @do_twice def greet(name): print(f"Hello {name}") ``` ```python >>> greet("World") Hello World Hello World ``` --- #### Returning values from decorated functions ```python @do_twice def return_greeting(name): print("Creating greeting") return f"Hi {name}" ``` ```python >>> hi_adam = return_greeting("Adam") Creating greeting Creating greeting >>> print(hi_adam) # ??? ``` <br> --- #### Returning values from decorated functions ```python @do_twice def return_greeting(name): print("Creating greeting") return f"Hi {name}" ``` ```python >>> hi_adam = return_greeting("Adam") Creating greeting Creating greeting >>> print(hi_adam) None ``` <p style='text-align: justify; font-size: 20pt'> Because the do_twice_wrapper() doesn’t explicitly return a value, the call return_greeting("Adam") ended up returning None. </p> --- #### Returning values from decorated functions ```python def do_twice(func): def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) return func(*args, **kwargs) return wrapper_do_twice ``` ```python @do_twice def return_greeting(name): print("Creating greeting") return f"Hi {name}" ``` ```python >>> return_greeting("Adam") Creating greeting Creating greeting 'Hi Adam' ``` --- ### Introspection ```python >>> print <built-in function print> >>> print.__name__ 'print' ``` ```python >>> say_whee <function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0> >>> say_whee.__name__ 'wrapper_do_twice' ``` --- #### @functools.wraps decorator ```python import functools def do_twice(func): @functools.wraps(func) def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) return func(*args, **kwargs) return wrapper_do_twice ``` ```python >>> say_whee <function say_whee at 0x7ff79a60f2f0> >>> say_whee.__name__ 'say_whee' >>> help(say_whee) Help on function say_whee in module whee: say_whee() ``` --- ### Decorator template ```python import functools def decorator(func): @functools.wraps(func) def wrapper_decorator(*args, **kwargs): # Do something before value = func(*args, **kwargs) # Do something after return value return wrapper_decorator ``` --- ### Decorators with arguments ```python def repeat(num_times): def decorator_repeat(func): @functools.wraps(func) def wrapper_repeat(*args, **kwargs): for _ in range(num_times): value = func(*args, **kwargs) return value return wrapper_repeat return decorator_repeat ``` ```python @repeat(num_times=3) def greet(name): print(f"Hello {name}") ``` ```python >>> greet("World") Hello World Hello World Hello World ``` --- #### Decorators with or without arguments ```python def repeat(_func=None, *, num_times=2): def decorator_repeat(func): @functools.wraps(func) def wrapper_repeat(*args, **kwargs): for _ in range(num_times): value = func(*args, **kwargs) return value return wrapper_repeat if _func is None: return decorator_repeat else: return decorator_repeat(_func) ``` <a href="https://realpython.com/primer-on-python-decorators/#both-please-but-never-mind-the-bread" style="font-size: 18pt;">more details (link)</a> --- ```python @repeat def say_whee(): print("Whee!") @repeat(num_times=3) def greet(name): print(f"Hello {name}") ``` ```python >>> say_whee() Whee! Whee! >>> greet("Penny") Hello Penny Hello Penny Hello Penny ``` --- ### Stateful Decorators ```python import functools def count_calls(func): @functools.wraps(func) def wrapper_count_calls(*args, **kwargs): wrapper_count_calls.num_calls += 1 print( f"Call {wrapper_count_calls.num_calls}" " of {func.__name__!r}" ) return func(*args, **kwargs) wrapper_count_calls.num_calls = 0 return wrapper_count_calls ``` ```python @count_calls def say_whee(): print("Whee!") ``` --- <p style='text-align: justify; font-size: 20pt'> The state - the number of calls to the function - is stored in the function attribute .num_calls on the wrapper function. Here is the effect of using it: </p> ```python >>> say_whee() Call 1 of 'say_whee' Whee! >>> say_whee() Call 2 of 'say_whee' Whee! >>> say_whee.num_calls 2 ``` --- ### @ The end --- --- ### Classes as Decorators ```python import functools class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.num_calls = 0 def __call__(self, *args, **kwargs): self.num_calls += 1 print( f"Call {self.num_calls}" " of {self.func.__name__!r}") return self.func(*args, **kwargs) ``` ```python @CountCalls def say_whee(): print("Whee!") ``` --- ### Creating Singletons ```python import functools def singleton(cls): """Make a class a Singleton class (only one instance)""" @functools.wraps(cls) def wrapper_singleton(*args, **kwargs): if not wrapper_singleton.instance: wrapper_singleton.instance = cls(*args, **kwargs) return wrapper_singleton.instance wrapper_singleton.instance = None return wrapper_singleton @singleton class TheOne: pass ``` --- ```python >>> first_one = TheOne() >>> another_one = TheOne() >>> id(first_one) 140094218762280 >>> id(another_one) 140094218762280 >>> first_one is another_one True ``` --- ### Thank you
{"metaMigratedAt":"2023-06-15T01:05:05.252Z","metaMigratedFrom":"YAML","title":"Python Decorators","breaks":false,"slideOptions":"{\"transition\":\"none\",\"progress\":false,\"slideNumber\":true,\"theme\":\"league\"}","contributors":"[{\"id\":\"afb6236d-117e-49ec-841e-c28a3996a432\",\"add\":20731,\"del\":7155}]"}
    1308 views