Commit b0ea9027 authored by Olivier Iffrig's avatar Olivier Iffrig

New tutorial: decorators (french preliminary version)

parent 299cd59c
\documentclass{article}
% Auteur : Olivier Iffrig
% Copyright : 2015 Olivier Iffrig
% Licence : CC-BY-SA (http://creativecommons.org/licenses/by-sa/4.0/)
\usepackage[utf8]{inputenc}
\usepackage[greek,french]{babel}
\usepackage[usenames,svgnames]{xcolor}
\usepackage[pdftex]{hyperref}
\usepackage{palatino}
\usepackage{graphicx}
\usepackage{listings}
\usepackage{tikz}
\usepackage{geometry}
\geometry{hmargin=1.5in}
\hypersetup{colorlinks,%
%citecolor=black,%
%filecolor=black,%
linkcolor=blue,%
urlcolor=blue%
}
\title{Les décorateurs}
\author{Olivier Iffrig}
\date{\today}
\lstset{literate=
{á}{{\'a}}1 {é}{{\'e}}1 {í}{{\'i}}1 {ó}{{\'o}}1 {ú}{{\'u}}1
{Á}{{\'A}}1 {É}{{\'E}}1 {Í}{{\'I}}1 {Ó}{{\'O}}1 {Ú}{{\'U}}1
{à}{{\`a}}1 {è}{{\`e}}1 {ì}{{\`i}}1 {ò}{{\`o}}1 {ù}{{\`u}}1
{À}{{\`A}}1 {È}{{\'E}}1 {Ì}{{\`I}}1 {Ò}{{\`O}}1 {Ù}{{\`U}}1
{ä}{{\"a}}1 {ë}{{\"e}}1 {ï}{{\"i}}1 {ö}{{\"o}}1 {ü}{{\"u}}1
{Ä}{{\"A}}1 {Ë}{{\"E}}1 {Ï}{{\"I}}1 {Ö}{{\"O}}1 {Ü}{{\"U}}1
{â}{{\^a}}1 {ê}{{\^e}}1 {î}{{\^i}}1 {ô}{{\^o}}1 {û}{{\^u}}1
{Â}{{\^A}}1 {Ê}{{\^E}}1 {Î}{{\^I}}1 {Ô}{{\^O}}1 {Û}{{\^U}}1
{œ}{{\oe}}1 {Œ}{{\OE}}1 {æ}{{\ae}}1 {Æ}{{\AE}}1 {ß}{{\ss}}1
{ç}{{\c c}}1 {Ç}{{\c C}}1 {ø}{{\o}}1 {å}{{\r a}}1 {Å}{{\r A}}1
{}{{\EUR}}1 {£}{{\pounds}}1
}
\lstset{ %
language=Python, %
keywordstyle=\color{DarkBlue}, %
keywordstyle=[2]\color{DarkGreen}, %
morekeywords={as,with}, %
morekeywords=[2]{bytes}, %
stringstyle=\color{DarkRed}, %
showstringspaces=false %
}
\lstdefinelanguage{PythonError}{%
breaklines=true,%
sensitive=true,%
keywordstyle=\color{Red},%
keywordstyle=[2]\color{Orange},
morekeywords={ArithmeticError,AssertionError,AttributeError,BaseException,%
BufferError,EOFError,EnvironmentError,Exception,FloatingPointError,%
GeneratorExit,IOError,ImportError,IndentationError,IndexError,KeyError,%
KeyboardInterrupt,LookupError,MemoryError,NameError,%
NotImplementedError,OSError,OverflowError,ReferenceError,RuntimeError,%
StandardError,StopIteration,SyntaxError,SystemError,SystemExit,TabError,%
TypeError,UnboundLocalError,UnicodeDecodeError,UnicodeEncodeError,%
UnicodeError,UnicodeTranslateError,ValueError,ZeroDivisionError},%
morekeywords=[2]{BytesWarning,DeprecationWarning,FutureWarning,%
ImportWarning,PendingDeprecationWarning,RuntimeWarning,SyntaxWarning,%
UnicodeWarning,UserWarning,Warning}%
}
\newcommand\gpic[3]{\begin{center}\includegraphics[#1]{#2}\\* #3\end{center}}
\newcommand\cpic[2]{\gpic{height=4cm}{#1}{#2}}
\newcommand\Cpic[2]{\cpic{#1}{\rule{0pt}{2ex} \tiny #2}}
\newcommand\pic[1]{\cpic{#1}{}}
\newcommand\vcpic[2]{\gpic{width=4cm}{#1}{#2}}
\newcommand\vCpic[2]{\vcpic{#1}{\rule{0pt}{2ex} \tiny #2}}
\newcommand\vpic[1]{\vcpic{#1}{}}
\newcommand\cc[1]{{\footnotesize #1}}
\begin{document}
\maketitle
\section*{Licence}
Cet article, ainsi que les images qu'il contient (sauf mention contraire
explicite) sont sous licence Creative Commons CC-BY-SA. Vous pouvez le copier et
le modifier à votre guise, à condition de citer l'auteur, de mettre en évidence
vos modifications et de partager les modifications sous la même licence. Pour
plus de détails : \url{http://creativecommons.org/licenses/by-sa/4.0/}
\section{C'est quoi ?}
Un décorateur, c'est une fonction qui permet de modifier des fonctions. L'idée,
c'est de pouvoir ajouter des fonctionnalités à des fonctions sans avoir à
rajouter des choses dans le corps desdites fonctions.
Supposons par exemple que l'on ait des fonctions qui prennent un entier en
paramètre. On voudrait s'assurer que l'utilisateur passe bien un entier. La
méthode naïve est d'ajouter le test au corps de la fonction :
\begin{lstlisting}
def myfunc(x):
if not isinstance(x, int):
raise TypeError("Expected an integer, got '%r'" % type(x))
return x + 2
\end{lstlisting}
Cette méthode a plusieurs inconvénients :
\begin{itemize}
\item il faut modifier le corps de chaque fonction que l'on définit,
\item ça rajoute du code qui n'a rien à voir avec la logique de la fonction
à proprement parler.
\end{itemize}
On peut donc se dire qu'il suffit de tester le type avant d'appeler la fonction.
\begin{lstlisting}
def myfunc(x):
return x + 2
#...
if not isinstance(x, int):
raise TypeError("Expected an integer, got '%r'" % type(x))
y = myfunc(x)
\end{lstlisting}
Dans ce cas, on compte sur l'utilisateur de la fonction pour faire le test. Rien
ne garantit qu'il ne sera pas oublié. De plus, ça ne règle pas le problème du
code dupliqué si l'on a plusieurs fonctions de ce genre. Mais on peut s'en
servir pour faire les choses plus intelligemment : pourquoi ne ferait-on pas une
fonction qui se fait passer pour notre fontion \lstinline{myfunc}, qui
vérifierait le type de son argument avant d'appeler la vraie fonction.
\begin{lstlisting}
def _myfunc(x):
return x + 2
def myfunc(x):
if not isinstance(x, int):
raise TypeError("Expected an integer, got '%r'" % type(x))
return _myfunc(x)
\end{lstlisting}
On a ainsi un test systématique du type de l'argument à l'appel de
\lstinline{myfunc}, sans interférer avec le code. Cependant, si on a plusieurs
fonctions, il faut toujours dupliquer du code. On peut néanmoins remarquer
quelque chose d'intéressant : la fonction \og englobante \fg{} est totalement
générique. On peut donc définir une fonction, qui prendrait en paramètre une
autre fonction \lstinline{func}, et qui renverrait une fonction
\lstinline{wrapper_func}, qui testerait le type de son argument avant d'appeler
\lstinline{func} :
\begin{lstlisting}
def int_function(func):
def wrapper_func(x):
if not isinstance(x, int):
raise TypeError("Expected an integer, got '%r'" % type(x))
return func(x)
return wrapper_func
def _myfunc(x):
return x + 2
myfunc = int_function(_myfunc)
\end{lstlisting}
Félicitations, vous venez d'écrire votre premier décorateur. Python a une
syntaxe particulière pour les décorateurs, qui permet de rendre la notation plus
compacte, et de bien mettre en évidence le lien entre la fonction décorée et la
fonction résultante (en plus de lui donner le même nom) :
\begin{lstlisting}
def int_function(func):
def wrapper_func(x):
if not isinstance(x, int):
raise TypeError("Expected an integer, got '%r'" % type(x))
return func(x)
return wrapper_func
@int_function
def myfunc(x):
return x + 2
#...
y = myfunc(x)
\end{lstlisting}
\section{Décorateurs avec arguments}
Dans la section précédente, nous avons écrit un décorateur qui vérifie si
l'argument d'une fonction est un entier avant d'appeler cette fonction. On
pourrait être tenté de gé\-\-ra\-li\-ser à n'importe quel type, avec un
décorateur qui accepte un paramètre : le type attendu. Il est possible de faire
ça, en créant une fonction prenant en paramètre ce type, et renvoyant le
décorateur correspondant :
\begin{lstlisting}
def check_type(type_):
def decorator(func):
def wrapper_func(x):
if not isinstance(x, type_):
raise TypeError("Expected '%r', got '%r'" % (type_, type(x)))
return func(x)
return wrapper_func
return decorator
int_function = check_type(int)
@int_function
def myfunc(x):
return x + 2
#...
y = myfunc(x)
\end{lstlisting}
Et en fait, la syntaxe des décorateurs permet de simplifier la notation en
appliquant directement \lstinline{check_type(int)} comme décorateur, plutôt que
de créer la variable \lstinline{int_function}.
\begin{lstlisting}
def check_type(type_):
def decorator(func):
def wrapper_func(x):
if not isinstance(x, type_):
raise TypeError("Expected '%r', got '%r'" % (type_, type(x)))
return func(x)
return wrapper_func
return decorator
@check_type(int)
def myfunc(x):
return x + 2
#...
y = myfunc(x)
\end{lstlisting}
\section{Introspection}
Une des grandes forces de Python, c'est la possibilité d'introspection : on peut
par exemple récupérer le nom ou la documentation d'une fonction. Que se
passe-t-il si l'on essaie sur une fonction décorée ?
\begin{lstlisting}
def check_type(type_):
def decorator(func):
def wrapper_func(x):
if not isinstance(x, type_):
raise TypeError("Expected '%r', got '%r'" % (type_, type(x)))
return func(x)
return wrapper_func
return decorator
@check_type(int)
def myfunc(x):
"""Adds two to the argument. It should be an integer."""
return x + 2
print(myfunc.__name__) # wrapper_func
print(myfunc.__doc__) # None
\end{lstlisting}
On a remplacé la fonction par sa version décorée. Si l'on veut permettre
l'introspection, il faut donc modifier la fonction décorée.
\begin{lstlisting}
def check_type(type_):
def decorator(func):
def wrapper_func(x):
if not isinstance(x, type_):
raise TypeError("Expected '%r', got '%r'" % (type_, type(x)))
return func(x)
wrapper_func.__name__ = func.__name__
wrapper_func.__doc__ = func.__doc__
return wrapper_func
return decorator
@check_type(int)
def myfunc(x):
"""Adds two to the argument. It should be an integer."""
return x + 2
print(myfunc.__name__) # myfunc
print(myfunc.__doc__) # Adds two to the argument. It should be an integer.
\end{lstlisting}
Ne pourrait-on pas rendre ça plus systématique ? Ça ressemble précisément au cas
d'usage d'un décorateur :
\begin{lstlisting}
def wraps(wrapped_func):
def decorator(wrapper_func):
wrapper_func.__name__ = wrapped_func.__name__
wrapper_func.__doc__ = wrapped_func.__doc__
return wrapper_func
return decorator
def check_type(type_):
def decorator(func):
@wraps(func)
def wrapper_func(x):
if not isinstance(x, type_):
raise TypeError("Expected '%r', got '%r'" % (type_, type(x)))
return func(x)
return wrapper_func
return decorator
\end{lstlisting}
Un décorateur comme celui que nous venons de définir existe, il s'agit de
\href{https://docs.python.org/3/library/functools.html#functools.wraps}{functools.wraps}.
\end{document}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment