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 ?
- Le projet django-class-based-views de Ben Fishman
- Le projet django-clsview de Zachary Voase
- La branche class-based-generic-views de Jacob Kaplan-Moss
- Les slides de la conférence de Simon Willison sur le sujet
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é.