\documentclass[14pt,compress,aspectratio=169]{beamer} \usepackage{hyperref} \input{macros.tex} \title[Decorators]{Advanced Python} \subtitle{Decorators} \author[FOSSEE] {The FOSSEE Group} \institute[IIT Bombay] {Department of Aerospace Engineering\\IIT Bombay} \date[] {Mumbai, India} \begin{document} \begin{frame} \titlepage \end{frame} \begin{frame}[fragile] \frametitle{Overview} Decorators: \begin{itemize} \item transform a function/method using a function \item are higher order functions \item provide a handy syntax \item are a powerful feature \item used by many libraries \end{itemize} \end{frame} \begin{frame}[fragile] \frametitle{Trivial example} \begin{lstlisting} def deco(func): return func @deco def greet(): print("Namaste!") # @deco is equivalent to: greet = deco(greet) \end{lstlisting} \end{frame} \begin{frame}[fragile] \frametitle{Understanding a bit more} \begin{lstlisting} def deco(func): print("deco") # <-- return func @deco def greet(): print("Namaste!") greet = deco(greet) \end{lstlisting} \end{frame} \begin{frame} \frametitle{Observations} \begin{itemize} \item Decorator is a convenient syntax \item Called when the function is defined \item Not called every time the decorated function is called \end{itemize} \end{frame} \begin{frame}[fragile, plain] \frametitle{Non-trivial example} Modify the function to print ``Hello'' before the function is called \begin{lstlisting} def deco(func): def new_func(*args, **kw): print("Hello") return func(*args, **kw) return new_func \end{lstlisting} \pause \begin{lstlisting} @deco def greet(): '''Print greeting.''' print("Namaste!") In []: greet() \end{lstlisting} \end{frame} \begin{frame} \frametitle{Observations} \begin{itemize} \item Notice the use of the \py{*args, **kw} \item The target function may have any arguments \item We replace the original function with \py{new_func} \item So does this cause any problems? \end{itemize} \end{frame} \begin{frame}[fragile] \frametitle{Issues} \begin{lstlisting} In []: greet.__name__ Out[]: 'new_func' In []: greet? In []: greet.__doc__ In []: greet.__module__ \end{lstlisting} \end{frame} \begin{frame}[fragile] \frametitle{Using \py{wraps}} \begin{itemize} \item Using \py{functools.wraps} prevents these problems \end{itemize} \begin{lstlisting} from functools import wraps # <-- def deco(func): @wraps(func) # <-- def new_func(*args, **kw): print("Hello") return func(*args, **kw) return new_func \end{lstlisting} Now try again. \end{frame} \begin{frame}[fragile] \frametitle{Decorators taking arguments} \begin{itemize} \item Sometimes you want to customize the decorator \item You wish to pass arguments to the decorator \end{itemize} \pause First attempt: \begin{lstlisting} from functools import wraps def deco(func, greet='Hello'): @wraps(func) def new_func(*args, **kw): print(greet) return func(*args, **kw) return new_func \end{lstlisting} \end{frame} \begin{frame}[fragile] \frametitle{Let us try} \small \begin{lstlisting} @deco def f(): print('Hi') In []: f() Hello Hi \end{lstlisting} \pause \begin{lstlisting} @deco(greet='Namaste') def f(): print('Hi') TypeError: deco() got an unexpected keyword argument 'greet' \end{lstlisting} \end{frame} \begin{frame} \frametitle{Fixing the problem} \begin{itemize} \item \py{deco(greet='Namaste')} is not passed the function! \item So \py{deco(greet='Namaste')} should return a decorator \vspace*{0.2in} \end{itemize} \pause \begin{block}{Solution} \begin{itemize} \item Must return a decorator when called without a function \item When passed a function just call the decorator \end{itemize} \end{block} \end{frame} \begin{frame}[fragile, plain] \frametitle{Fixing the problems} \small \begin{lstlisting} def deco(func=None, greet='Hello'): def wrapper(func): # <-- a decorator @wraps(func) def new_func(*args, **kw): print(greet) return func(*args, **kw) return new_func if func is None: return wrapper else: return wrapper(func) \end{lstlisting} \end{frame} \begin{frame}[fragile] \frametitle{Let us try} \small \begin{lstlisting} @deco def f(): print('Hi') In []: f() Hello Hi \end{lstlisting} \pause \begin{lstlisting} @deco(greet='Namaste') def f(): print('Hi') In []: f() Namaste Hi \end{lstlisting} \end{frame} \begin{frame} \frametitle{Summary} \begin{itemize} \item Creating simple decorators \item Using \py{functools.wraps} \item Decorators taking arguments \end{itemize} \end{frame} \begin{frame}[plain, fragile] \frametitle{Exercise: simplest decorator} \begin{block}{} Write a decorator called \py{greet} that prints \py{'Hello'} before the function is called. \end{block} \begin{lstlisting} @greet def f(x): print(x) In []: f(1) Hello 1 \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Solution} \begin{lstlisting} def greet(func): def new_func(*args, **kw): print("Hello") return func(*args, **kw) return new_func \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Exercise: add goodbye} \begin{block}{} Modify decorator \py{greet} to print ``goodbye'' after executing the function. \end{block} \begin{lstlisting} from functools import wraps def greet(func): @wraps(func) def new_func(*args, **kw): print("Hello") return func(*args, **kw) return new_func \end{lstlisting} \end{frame} \begin{frame}[fragile] \frametitle{Solution} \begin{lstlisting} def greet(func): @wraps(func) def new_func(*args, **kw): print("Hello") result = func(*args, **kw) print("goodbye") return result return new_func \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Exercise: print function name} \begin{block}{} Write a decorator called \py{debug()} that prints the name of the function before it is called. \textbf{Hint:} Recall \py{__name__} \end{block} \begin{lstlisting} @debug def my_func(x): print(x) In []: my_func(1) my_func 1 \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Solution} \begin{lstlisting} from functools import wraps def debug(func): @wraps(func) def new_func(*args, **kw): print(func.__name__) return func(*args, **kw) return new_func \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Exercise: print function name with optional message} \begin{block}{} Write a decorator called \py{debug(f, message='')} that prints the name of the function before it is called along with an optional message. \end{block} \begin{lstlisting} @debug(message='DEBUG: ') def my_func(x): print(x) In []: my_func(1) DEBUG: my_func 1 \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Solution} \vspace*{-0.1in} \small \begin{lstlisting} from functools import wraps def debug(func=None, message=''): def wrapper(func) @wraps(func) def new_func(*args, **kw): if message: print(message, func.__name__) else: print(func.__name__) return func(*args, **kw) return new_func if func: return wrapper(func) else: return wrapper \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Exercise: track number of calls} \begin{block}{} Write a decorator called \py{counter()} that keeps track of the number of times a decorated function is called. Write another function called \py{show_counts()} which lists all the called functions. \end{block} \small \begin{lstlisting} @counter def sq(x): return x*x @counter def g(x): return x+2 In []: for i in range(10): g(sq(i)) In []: show_counts() sq : 10 g : 10 \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Solution} \begin{lstlisting} _counter_data = {} def counter(func): @wraps(func) def new_func(*args, **kw): if func in _counter_data: _counter_data[func] += 1 else: _counter_data[func] = 1 return func(*args, **kw) return new_func \end{lstlisting} \end{frame} \begin{frame}[plain, fragile] \frametitle{Solution} \begin{lstlisting} def show_counts(): for f, c in _counter_data.items(): print(f.__name__, ':', c) \end{lstlisting} \end{frame} \begin{frame} \frametitle{Homework} \begin{block}{} Convert the above into a class so that the global \py{_counter_data} and the decorator is encapsulated into the class.\\ Hints: \begin{itemize} \item A decorator can be any callable, even a method \item You can override the \py{__call__} special method to make the object callable \end{itemize} \end{block} \end{frame} \end{document}