Version longue de ma réponse sur IndexError : polymorphisme en Python.

Depuis Python 3.4, le décorateur functools.singledispatch permet de définir une fonction dite générique et de créer, par la suite, plusieurs autres fonctions qui seront des adaptations de celle-ci suivant le type du paramètre donné.

Prenons un exemple ultra simple : une fonction qui affiche le contenu de la variable passée en paramètre.

def print_var(arg):
    print(type(arg).__name__, arg)

À ce moment là, on peut tester et voir que ça fonctionne :

>>> print_var(42)
int 42

>>> print_var(list(range(10)))
list [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Maintenant, pour changer le comportement de print_var en fonction du type du paramètre, il faut ajouter 2 lignes de code :

from functools import singledispatch

@singledispatch
def print_var(arg):
    """ Fonction de base dite générique. """
    print(type(arg).__name__, arg)

Note : il est important de savoir que le décorateur ne prendra en compte que le type du premier argument.

Ensuite, on crée les fonctions associées en utilisant le décorateur @nom_fonction.register(type_argument) :

@print_var.register(int)
def _(arg):
    """ La fonction reçoit un entier en entrée. """
    print(arg)

@print_var.register(list)
@print_var.register(tuple)
def _(arg):
    """ La fonction reçoit soit une list, soit un tuple en entrée. """
     for idx, item in enumerate(arg):
         print(' ' * idx + str(item))

@print_var.register(range)
def _(arg):
    """ La fonction reçoit soit un générateur d'intervalle en entrée. """
    print(arg)
    for item in arg:
        print(' ' * 6 + str(item))

On peut tester et voir que ça fonctionne :

>>> print_var(42)
42

>>> print_var(list(range(10)))
0
 1
  2
   3
    4
     5
      6
       7
        8
         9

>>> print_var(range(0, 10, 2))
range(0, 10, 2)
      0
      2
      4
      6
      8

>>> print_var({ 'réponse': 42 })
dict {'réponse': 42}
# Le type dict n'ayant pas de fonction associée, c'est donc la fonction de base qui est appelée.
# Vous pouvez vous entrainer à implémentant ce type ;)

On peut aller plus loin en acceptant tout type générique. Je parle des classes de base définies dans collections.abc :

from collections.abc import Sequence

@print_var.register(Sequence)
def _(arg):
    """ La fonction reçoit une séquence en entrée.
        Ce qui peut être un type built-in (list, dict, tuple, set, etc.) 
        ou un type crée qui hérite du type Sequence ou d'un built-in
        qui hérite lui-même du type Sequence.
    """
    for item in arg:
        print(item)

Ou encore prendre en compte tout type de nombre en utilsant numbers.Number :

from numbers import Number

@print_var.register(Number)
def _(arg):
    """ La fonction reçoit un nombre en entrée.
        Ce qui peut être un type built-in (int, float, complex, etc.) 
        ou un type crée qui hérite du type Number ou d'un built-in
        qui hérite lui-même du type Number.
    """
    print(arg)

Et bien entendu, vous pouvez utiliser vos propres types, aussi farfelus soient-ils.


Python 3.7

Avec Python 3.7, il sera possible d'utiliser le décorateur sans préciser le type du 1er argument. Il se basera sur la signature de la fonction, ce qui donnerait, en utilisant l'exemple précédent :

from numbers import Number

@print_var.register
def _(arg: Number):
    print(arg)

Sources :


Historique

  • 2017-12-15 : Ajout du décorateur prenant en compte le type du 1er argument.