Avatar

Gilles Fabio

Full-Stack Developer

Python et les "callables"

Tout développeur Python s’est certainement en jour posé cette question : qu’est-ce qu’un callable au sens Python ? Tout objet “exécutable” ou “appelable” ? Flou. Vague. En Python, les fonctions et les méthodes sont “naturellement” des callables puisqu’elles sont définies pour être appelées. On dit d’ailleurs qu’un objet callable est un objet qui peut être appelé comme une fonction (avec des parenthèses et prenant, éventuellement, des arguments).

Si l’on se réfère à la documentation officielle, sont callables :

  • Les fonctions et méthodes que nous définissons (user-defined)
  • Les fonctions et méthodes natives (built-in)
  • Les fonctions generator
  • Les classes new-style ou old-style (nommées aussi classic)
  • Les instances de classe possédant une méthode __call__

En effet, Python ne limite pas qu’aux fonctions, classes et méthodes. Un callable est aussi tout objet qui possède une méthode spécifique nommée __call__. Pour être plus précis : un objet dont le type possède une méthode spécifique nommée __call__. Si l’objet est appelée comme une fonction, il retournera la valeur retournée par cette méthode.

Les deux exemples ci-dessous sont similaires :

# 1er exemple : une fonction
# -------------------------------------------------------------------------
def foo():
    """
    Callable affichant la chaîne de caractères "Hello".
    """
    print u"Hello"

# Ici, on appelle directement la fonction
foo()

# 2ème exemple : une classe avec méthode "__call__"
# -------------------------------------------------------------------------
class Foo(object):
    """
    Callable affichant la chaîne de caractères "Hello".
    """

    def __call__(self):
        print u"Hello"

# Ici, on instancie la classe et on appelle l'objet comme une fonction
foo = Foo()
foo()

Grâce à cette fonctionnalité, il est alors tout à fait possible d’utiliser les instances d’une classe comme des fonctions. À la seule différence qu’une instance de classe préserve l’état de ses variables (ce qui permet de modifier ses données à tout moment) et permet d’effectuer d’autres opérations (via différentes méthodes), pas seulement un appel à __call__.

Exemple :

# -*- coding: utf-8 -*-
class Add(object):
    """
    Callable stockant un compteur que nous pouvons incrémenter à chaque
    appel en lui donnant comme argument un entier.
    """

    def __init__(self):
        """
        Initialisation du compteur.
        """
        self.count = 0

    def __call__(self, value=None):
        """
        Méthode exécutée si l'objet est appelé comme une fonction.
        """
        if value is not None:
            self.count += int(value)
        return self.count

    def reset(self):
        """
        Remet à zéro le compteur.
        """
        self.count = 0

add = Add()

print add()
print add(10)
print add()
print add(15)
add.reset()
print add()
print add(2)

Si on exécute ce code :

0
10
10
25
0
2

Dans cet exemple, on crée une instance dont le compteur est initialisé à zéro. Si on appelle cette instance comme une fonction, et si on ne lui donne pas d’argument, elle renvoie l’état actuel du compteur. Si on lui donne un entier, elle incrémente son état. L’état est préservé. On peut également effectuer d’autres opérations sur l’objet, dont notamment la remise à zéro du compteur.

L’utilisation de __call__ est, notamment, très intéressante avec le framework web Django. Son système d’URL accepte n’importe quel callable, pas seulement des fonctions. Il est alors possible d’implémenter des vues reposant sur des classes. On appelle ce type de vue : class-based. Les applications admin et syndication utilisent cette technique. D’autres exemples ?

Pour savoir si un objet est callable ou pas, il est tentant d’utiliser la fonction native callable, incluse dans Python 2.x. Elle prend pour argument l’objet à tester. Si elle renvoie True, l’objet est bien callable.

Exemple avec une fonction et un entier :

>>> def foo():
...     pass
...
>>> callable(foo)
True
>>> i = 3
>>> callable(i)
False

Mais cette fonction a été supprimée dans la version 3 du langage. On peut, cependant, aisément la remplacer par une fonction lambda qui vérifie si l’objet passé en argument possède ou non un attribue nommé __call__ :

>>> callable = lambda o: hasattr(o, '__call__')
>>> callable(foo)
True
>>> callable(i)
False

En revanche, si les classes new-style héritant d’object disposent par défaut d’une méthode __call__, les classes old-style (ou classic) n’héritant pas d’object n’ont pas de méthode __call__ mais sont considérées comme callable (puisqu’elles retournent une instance). C’est un point important puisque notre fonction lambda appliquée sur une classe old-style renverra systématiquement False.

Exemple :

>>> is_callable = lambda o: hasattr(o, '__call__')
>>> class Foo:
...     pass
...
...
>>> callable(Foo)
True
>>> is_callable(Foo)
False

On remarque bien dans cet exemple que notre fonction lambda considère notre classe old-style comme non-callable, alors que la fonction native callable la considère comme callable. Méfiance. Cela peut être source de bogue. Le plus sage serait de bannir définitivement les classes old-style puisqu’elles ont disparu dans la version 3 du langage. Ou de continuer à utiliser la fonction callable (ou votre propre implémentation), si vous utilisez des classes old-style avec une version 2.x.

Précision. Si une classe new-style est callable par défaut, on parle bien de la “classe” et non de ses éventuelles instances. Si on instancie un objet à partir de cette classe, il ne possédera pas de méthode __call__, sauf si nous la définissons explicitement.

Preuve à l’appui :

>>> class Bar(object):
...     pass
...
>>> hasattr(Bar, '__call__')
True
>>> bar = Bar()
>>> hasattr(bar, '__call__')
False
>>> class Bar(object):
...     def __call__(self):
...         pass
...
>>> bar = Bar()
>>> hasattr(bar, '__call__')
True

Maintenant qu’on s’est familiarisé avec les callables, faisons quelques tests.

# -*- coding: utf-8 -*-

class OldStyle:
    foo = "attribut foo"

    def bar(self):
        pass

class OldStyleCall:
    foo = "attribut foo"
    def __call__(self):
        pass

    def bar(self):
        pass

class NewStyle(object):
    foo = "attribut foo"

    def bar(self):
        pass

class NewStyleCall(object):
    foo = "attribut foo"

    def __call__(self):
        pass

    def bar(self):
        pass

old_style = OldStyle()
old_style_call = OldStyleCall()
new_style = NewStyle()
new_style_call = NewStyleCall()

generator_expr = (2*x for x in (1, 2, 3))

def generator_func(n):
    while n > 0:
        yield n
        n -= 1

def function():
    pass

lambda_func = lambda x: x

TEST_OBJECTS = (
    ('sep', u'Types natifs'),
    (u"Entier",  10),
    (u"Flottant", 10.9),
    (u"Chaîne de caractères", u"Hello"),
    (u"Booléen", True),
    (u"Liste", [1, 2, 3]),
    (u"Tuple", (1, 2, 3)),
    (u"Dictionnaire", {u"1": 1, u"2": 2, u"3": 3}),

    ('sep', u'Generators'),
    (u"Expression", generator_expr),
    (u"Fonction", generator_func),

    ('sep', u'Fonctions'),
    (u"Fonction", function),
    (u"Fonction lambda", lambda_func),

    ('sep', u'Classes "Old-Style"'),
    (u"Classe", OldStyle),
    (u"Méthode de classe", OldStyle.bar),
    (u"Attribut de classe", OldStyle.foo),

    ('sep', u'Classes "Old-Style" avec méthode "__call__"'),
    (u"Classe", OldStyleCall),
    (u"Méthode de classe", OldStyleCall.bar),
    (u"Attribut de classe", OldStyleCall.foo),

    ('sep', u'Classes "New-Style"'),
    (u"Classe", NewStyle),
    (u"Méthode de classe", NewStyle.bar),
    (u"Attribut de classe", NewStyle.foo),

    ('sep', u'Classes "New-Style" avec méthode "__call__"'),
    (u"Classe", NewStyleCall),
    (u"Méthode de classe", NewStyleCall.bar),
    (u"Attribut de classe", NewStyleCall.foo),

    ('sep', u'Instance de classe "Old-Style"'),
    (u"Instance", old_style),
    (u"Méthode d'instance", old_style.bar),
    (u"Attribut d'instance", old_style.foo),

    ('sep', u'Instance de classe "Old-Style" avec méthode "__call__"'),
    (u"Instance", old_style_call),
    (u"Méthode d'instance", old_style_call.bar),
    (u"Attribut d'instance", old_style_call.foo),

    ('sep', u'Instance de classe "New-Style"'),
    (u"Instance", new_style),
    (u"Méthode d'instance", new_style.bar),
    (u"Attribut d'instance", new_style.foo),

    ('sep', u'Instance de classe "New-Style" avec méthode "__call__"'),
    (u"Instance", new_style_call),
    (u"Méthode d'instance", new_style_call.bar),
    (u"Attribut d'instance", new_style_call.foo),
    )

is_callable = lambda o: u"OUI" if callable(o) else u"NON"

for k, v in TEST_OBJECTS:
    if k == 'sep':
        print ""
        print v
        print "-" * 54
    else:
        print u"%s : %s" % (k, is_callable(v))

Vérifions :

Types natifs
------------------------------------------------------
Entier : NON
Flottant : NON
Chaîne de caractères : NON
Booléen : NON
Liste : NON
Tuple : NON
Dictionnaire : NON

Generators
------------------------------------------------------
Expression : NON
Fonction : OUI

Fonctions
------------------------------------------------------
Fonction : OUI
Fonction lambda : OUI

Classes "Old-Style"
------------------------------------------------------
Classe : OUI
Méthode de classe : OUI
Attribut de classe : NON

Classes "Old-Style" avec méthode "__call__"
------------------------------------------------------
Classe : OUI
Méthode de classe : OUI
Attribut de classe : NON

Classes "New-Style"
------------------------------------------------------
Classe : OUI
Méthode de classe : OUI
Attribut de classe : NON

Classes "New-Style" avec méthode "__call__"
------------------------------------------------------
Classe : OUI
Méthode de classe : OUI
Attribut de classe : NON

Instance de classe "Old-Style"
------------------------------------------------------
Instance : NON
Méthode d'instance : OUI
Attribut d'instance : NON

Instance de classe "Old-Style" avec méthode "__call__"
------------------------------------------------------
Instance : OUI
Méthode d'instance : OUI
Attribut d'instance : NON

Instance de classe "New-Style"
------------------------------------------------------
Instance : NON
Méthode d'instance : OUI
Attribut d'instance : NON

Instance de classe "New-Style" avec méthode "__call__"
------------------------------------------------------
Instance : OUI
Méthode d'instance : OUI
Attribut d'instance : NON

Finalement, rien de bien compliqué.