modifié le

Callables

1. Callable

Un callable est un objet que l’on appelle avec une syntaxe identique à l’appel de fonction, c’est-à-dire avec des () entourant d’éventuels arguments. Les fonctions sont des callables, les méthodes et les classes aussi. On peut vérifier qu’un objet est un callable à l’aide de la fonction booléenne callable() :

def f(): pass

class C:
  def m(self): pass

if __name__=="__main__":
  c = C()

  print(callable(f))        # True
  print(callable(f()))      # False
  print(callable(c))        # False
  print(callable(C))        # True
  print(callable(c.m))      # True

1.1. Créer un callable

Pour créer un callable, il suffit de créer une classe sachant interpréter (). C’est le but de la méthode o.__call__().

En effet, si o est une instance de classe, alors o() est interprété comme o.__call__(), o(1) est interprété comme o.__call__(1), o(1,2) est interprété comme o.__call__(1,2), etc.

Syntaxiquement, o.__call__() accepte donc un nombre quelconque d’arguments.

Exemple :

class Multiplicateur:
  def __init__(self,n):
    self.n = n
  def __call__(self,i):
    return i*self.n

if __name__=="__main__":
  tripleur = Multiplicateur(3)
  quadrupleur = Multiplicateur(4)

  for k in range(5):
    print(tripleur(k), quadrupleur(k)) (1)
1 ici, les objets tripleur et quadrupleur sont utilisés comme des fonctions

ce qui donne :

0  0
3  4
6  8
9 12
12 16

1.2. Arguments et paramètres

Les paramètres d’un callable sont spécifiés à sa définition, ce qui impose que des arguments soient fournis à l’appel pour les initialiser :

def f(a, b, c, d=1, e=2):   (1)
  print(f"{a=} {b=} {c=} {d=} {e=}")

f(3, 4, 5)            # a=3 b=4 c=5 d=1 e=2
f(3, 4, 5, 6)         # a=3 b=4 c=5 d=6 e=2
f(3, 4, 5, e=7, d=6)  # a=3 b=4 c=5 d=6 e=7
f(3, 4, d=6, c=5)     # a=3 b=4 c=5 d=6 e=2 (2)
1 les paramètres d et e possèdent une valeur par défaut : ils se placent obligatoirement après les autres.
2 3 et 4 sont des arguments positionnels, car ils sont repérés par leur position (et sont alors associés aux paramètres de même rang) ; 5 et 6 sont des arguments nommés, car leur valeur est précédée d’un nom de paramètre : ils peuvent ainsi être placés dans n’importe quel ordre (pour peu qu’ils soient placés après les arguments positionnels).

1.3. Paramètre de type callable

Certaines fonctions builtin de Python acceptent des callables en paramètre, comme par exemple les fonctions min, max et sorted :

def f(a):
  return abs(a)

print(max(-1,-4,5,0,-6,4, key=f))        # -6
print(min(-1,-4,5,0,-6,4, key=f))        # 0
print(sorted([-1,-4,5,0,-6,4], key=f))   # [0, -1, -4, 4, 5, -6]

Pour réaliser leur tâche, ces fonctions comparent non pas les valeurs de la séquence,mais les valeurs renvoyées par la fonction key. Par exemple :

max(-1,-4,5,0,-6,4, key=f)

compare abs(-1), abs(-4), abs(5), abs(0), abs(-6), abs(4) donc 1, 4, 5, 0, 6, 4. Le max est 6, donc max renvoie -6, la valeur originale.

De même,

sorted([-1,-4,5,0,-6,4], key=f)

renvoie les valeurs des éléments de { 1: -1, 4: -4, 5: 5, 0: 0, 6: -6, 4: 4 } triés dans l’ordre des clés.

Lambda

Une lambda est une fonction anonyme définie à la volée. Par exemple lambda x: x**2 définie la fonction x → x². Exemple :

f = lambda x,y : x**2+y**2     # f(x,y)=x²+y²

print(f(1,-1))   # 2

print(f)         # <function <lambda> at 0x7f0483cf9f30>
print(type(f))   # <class 'function'>

f est donc une fonction définie de façon particulière, mais globalement équivalente à :

def f(x,y):
  return x**2+y**2

L’intérêt d’une lambda, c’est donc justement de pouvoir créer une fonction à la volée, sans avoir à la définir avec un traditionnel “ def ... : ”. L’écriture s’en trouve allégée. Par exemple, lorsqu’une fonction attend un callable, on peut lui fournir directement le code :

print(max(-1,-4,5,0,-6,4, key=lambda x: abs(x)))
print(sorted([-1,-4,5,0,-6,4], key=lambda x: abs(1-x)))           # éloignement de 1
print(sorted([-1,-4,5,0,-6,4], key=lambda x: -1 if x<0 else 1 ))  # sépare les positifs des négatifs

1.4. L’opérateur splat

L’opérateur * permet de récupérer le tuple des arguments positionnels en surplus :

def f(a,b,*args):
  print(a,b,args)

f(1, 2, 3, 'a', 'b', None)   # (3, 'a', 'b', None) (1)
f(b=1, a=2)                  # 2 1 () (2)
f(b=1, a=2, c=3)             # TypeError: f() got an unexpected keyword argument 'c' (3)
1 1 et 2 passés dans a et b, args reçoit le reste, soit (3,'a','b',None)
2 2 et 1 passés dans a et b, pas d’autres arguments, donc *args est vide
3 2 et 1 passés dans a et b, reste 3 qui n’est pas positionnel
Le nom args n’est qu’une convention communément adoptée (au même titre que self pour le paramètre des méthodes). Dans l’absolu, son nom est quelconque.
Autre utilisation de splat

Utilisé seul, l’opérateur splat force les paramètres qui le suivent à ne recevoir que des arguments nommés :

def f(a, *, b):          (1)
    print(f"{a=} {b=}")

f(1, b=2)     # a=1 b=2
f(a=1, b=2)   # a=1 b=2
f(b=2, a=1)   # a=1 b=2
f(1,2)        # TypeError: f() takes 1 positional argument but 2 were given (2)
1 b ne pourra recevoir que des arguments nommés
2 le second argument n’est pas nommé → erreur

1.5. L’opérateur double-splat

L’opérateur double-splat récupère le dictionnaire des arguments en surplus (qui sont forcément des arguments nommés). Il doit se placer après tous les autres paramètres :

def f(a, b=10, **kwargs):
  print(f"{a=} {b=} {kwargs=}")

f(1)                  # a=1 b=10 kwargs={}
f(1, b=2)             # a=1 b=2 kwargs={}
f(a=1,b=2,c=3,d=4)    # a=1 b=2 kwargs={'c': 3, 'd': 4}
f(1,c=3,d=4)          # a=1 b=10 kwargs={'c': 3, 'd': 4}
f(1,c=3,d=4,b=2)      # a=1 b=2 kwargs={'c': 3, 'd': 4}
f(1,c=3,2,d=4)        # SyntaxError: positional argument follows keyword argument
Là aussi, le nom du paramètre n’a pas d’importance, kwargs n’est qu’une convention communément adoptée.

2. Décorateur

Un décorateur permet d’intercaler un traitement entre l’appel d’une fonction et son exécution. Il crée une fonction à partir d’une fonction quelconque f, en l’enrichissant par ajout de code avant et après (d’où le terme de décorateur). Par rapport à la fonction initiale f, la fonction décorée ajoute un pré-traitement et un post-traitement, ce qui permet par exemple :

  • de vérifier des conditions particulières avant d’exécuter une fonction, ou après en être sorti,

  • d’acquérir des ressources pour exécuter une fonction, et les libérer en fin d’exécution,

  • de tracer les appels de fonctions, la valeur des paramètres, etc.

Lorsqu’un programme agit dynamiquement sur son propre code, on parle de metaprogrammation.

Formellement, un décorateur est un callable recevant en paramètre un callable et renvoyant un nouveau callable. L’exemple suivant définit le plus simple des décorateurs :

def addition(a, b):
    return a + b

def multiplication(a, b):
    return a * b

def un_decorateur(fct): (1)
    return fct

f = un_decorateur(addition) (2)
print(f(1,2))  # 3

g = un_decorateur(multiplication) (3)
print(f(1,2))  # 2
1 Ce décorateur renvoie simplement la fonction qu’il a reçu.
2 equivalent à f=addition, puisque un_decorateur renvoie son argument tel quel
3 equivalent à g=multiplication, puisque un_decorateur renvoie son argument tel quel

Ce décorateur n’a pas d’intérêt, puisqu’il ne fait rien, il permet juste de comprendre le principe : s’intercaler entre l’appel d’une fonction et son exécution.

Plus intéressant serait un décorateur affichant le nom de la fonction à chaque appel à cette fonction. Si, pour cela, on ajoute un print() au décortateur précédent :

def nomme(fct):                          # le décorateur
  """ définition du décorateur """
  print(f"fonction {fct.__name__}:")
  return fct

alors le résultat n’est pas à la hauteur : le message sort lorsque la fonction est décorée, et non, comme espéré, lorsque la fonction décorée est appelée.

Pour obtenir le résultat attendu, le décorateur ne doit pas exécuter le print lui-même, mais le faire exécuter par la fonction qu’il décore :

def nomme(f_origin):
  def f_decoree(a,b):
    print(f"fonction {f_origin.__name__}")
    return f_origin(a,b)
  return f_decoree

def add(a, b):
    return a + b

def mult(a, b):
    return a * b

print(">> création des fonctions décorées")
add_d = nomme(add)
mult_d = nomme(mult)

print(">> appel aux fonctions décorées")
print(add_d(1,2))
print(mult_d(3,4))
print(mult_d(add_d(5,6),add_d(7,8)))

ce qui donne cette fois :

>> création des fonctions décorées
>> appel aux fonctions décorées
fonction add
3
fonction mult
12
fonction add
fonction add
fonction mult
165

2.1. L’opérateur pie @

L’opérateur @ écrase la fonction initiale par sa décoration. Etant donné un décorateur :

def un_decorateur(...)
  ...

alors écrire :

@un_decorateur
def f_origin(..):
  ...

est équivalent à :

def f_origin(...)
  ...

f_origin = un_decorateur(f_origin)

Donc @un_decorateur

  1. exécute le décorateur avec pour argument la fonction originale (celle qui est définie juste après)

  2. remplace la fonction originale par la fonction décorée

Exemple:

def nomme(f_origin):
  def f_decoree(a,b):
    print(f"fonction {f_origin.__name__}:")
    return f_origin(a,b)
  return f_decoree

@nomme
def add(a, b):
    return a + b

@nomme
def mul(a, b):
    return a * b

print(add(1,2))
print(mul(3,4))
print(mul(add(5,6),add(7,8)))

2.2. Paramétrer un décorateur

Pour paramétrer le décorateur lui-même, il suffit de créer une fonction qui retourne un décorateur, ce dernier renvoyant à son tour la fonction décorée. Exemple :

from time import sleep, gmtime, strftime

def log(fmt):
  def decorateur(f):
    def wrapper(*args,**kwargs):
      current_time = strftime(fmt, gmtime()) (1)
      print(f"[{current_time}] {f.__name__}{args}")
      return f(*args,**kwargs)
    return wrapper
  return decorateur
1 gmtime() renvoie l’instant actuel. strftime() transforme un instant en chaine de caractère suivant un certain format (fmt)

Ce décorateur log trace les appels de fonction avec un horodatage dont le format est un argument du décorateur :

@log("%T") (1)
def add(a,b):
  return a+b

@log("%D") (2)
def mul(a,b):
  return a*b
1 strftime("%T",instant) affiche le temps de l’instant
2 strftime("%D",instant) affiche la date de l’instant

Les fonctions décorées n’ont pas de particularités par rapport à précédemment :

print(add(1,2))
print(mul(3,4))
print(addl(mul(5,6),mul(7,8)))

donne :

[08:27:12] add(1, 2) (1)
3
[11/21/22] mul(3, 4) (2)
12
[11/21/22] mul(5, 6)
[11/21/22] mul(7, 8)
[08:27:13] add(30, 56)
86
1 l’horodatage de add est bien au format “%T”
2 l’horodatage de mul est bien au format “%D”

Les fonctions décorées par log affichent, à l’entrée, leur nom et la valeur de leurs paramètres, précédés d’un timestamp au format spécifié en argument au décorateur.