Nous avons durant ce cours étudié diverses interfaces (conteneurs, itérables, hashables, etc.). Nous allons maintenant voir comment les classes abstraites peuvent nous permettre de reconnaître ces interfaces.
Je ne reviendrai pas ici sur la notion même de classe abstraite, présentée dans cet autre cours, dont il est conseillé de prendre connaissance avant de passer à la suite.
Module abc
Pour rappel, le module abc
donne accès à la classe ABC
qui permet par héritage de construire une classe abstraite, et au décorateur abstractmethod
pour définir des méthodes abstraites.
Une autre classe importante réside dans ce module, ABCMeta
.
ABCMeta
est la métaclasse de la classe ABC
, et donc le type de toutes les classes abstraites.
C’est ABCMeta
qui s’occupe de référencer dans l’ensemble __abstractmethods__
1
les méthodes abstraites définies dans la classe.
Mais outre le fait de pouvoir spécifier les méthodes à implémenter, les classes abstraites de Python ont un autre but : définir une interface.
Vous connaissez probablement isinstance
, qui permet de vérifier qu’un objet est du bon type ;
peut-être moins issubclass
, pour vérifier qu’une classe hérite d’une autre.
1 2 3 4 5 6 7 8 | >>> isinstance(4, int) # 4 est un int True >>> isinstance(4, str) # 4 n'est pas une str False >>> issubclass(int, object) # int hérite d'object True >>> issubclass(int, str) # int n'hérite pas de str False |
Ces deux fonctions sont en fait des opérateurs, qui font appel à des méthodes spéciales, et sont à ce titre surchargeables, comme nous le verrons par la suite.
J’ai utilisé plus haut le terme « hérite » pour décrire l’opérateur issubclass
.
C’est en fait légèrement différent, issubclass
permet de vérifier qu’une classe est une sous-classe (ou sous-type) d’une autre.
Quand une classe hérite d’une autre, elle en devient un sous-type (sauf cas exceptionnels2). Mais elle peut aussi être sous-classe de classes dont elle n’hérite pas.
C’est le but de la méthode register
des classes ABC
.
Elle sert à enregistrer une classe comme sous-type de la classe abstraite.
Imaginons une classe abstraite Sequence
correspondant aux types de séquences connus (str
, list
, tuple
)3.
Ces types sont des builtins du langage, il ne nous est pas pas possible de les redéfinir pour les faire hériter de Sequence
.
Mais la méthode register
de notre classe abstraite Sequence
va nous permettre de les enregistrer comme sous-classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | >>> import abc >>> class Sequence(abc.ABC): ... pass ... >>> Sequence.register(str) <class 'str'> >>> Sequence.register(list) <class 'list'> >>> Sequence.register(tuple) <class 'tuple'> >>> isinstance('foo', Sequence) True >>> isinstance(42, Sequence) False >>> issubclass(list, Sequence) True >>> issubclass(dict, Sequence) False |
isinstance
Nous venons de voir que isinstance
était un opérateur, et qu’il était surcheargable.
Nous allons ici nous intéresser à la mise en œuvre de cette surcharge.
Pour rappel, la surchage d’opérateur se fait par la définition d’une méthode spéciale dans le type de l’objet.
Par exemple, il est possible d’utiliser +
sur le nombre 4
parce que 4
est de type int
, et qu’int
implémente la méthode __add__
.
isinstance
est un opérateur qui s’applique à une classe (la classe dont on cherche à savoir si tel objet en est l’instance).
La surcharge se fera donc dans le type de cette classe, c’est-à-dire dans la métaclasse.
La méthode spéciale correspondant à l’opérateur est __instancecheck__
, qui reçoit en paramètre l’objet à tester, et retourne un booléen (True
si l’objet est du type en question, False
sinon).
On peut par exemple imaginer une classe ABCIterable
, qui cherchera à savoir si un objet donné est itérable (possède une méthode __iter__
).
On teste pour cela si cet object a un attribut __iter__
, et si cet attribut est callable.
1 2 3 4 5 6 | class ABCIterableMeta(type): def __instancecheck__(self, obj): return hasattr(obj, '__iter__') and callable(obj.__iter__) class ABCIterable(metaclass=ABCIterableMeta): pass |
1 2 3 4 5 6 7 8 9 10 11 12 | >>> isinstance([], ABCIterable) True >>> isinstance((0,), ABCIterable) True >>> isinstance('foo', ABCIterable) True >>> isinstance({'a': 'b'}, ABCIterable) True >>> isinstance(18, ABCIterable) False >>> isinstance(object(), ABCIterable) False |
Quelques dernières précisions sur isinstance
: l’opérateur est un peu plus complexe que ce qui a été montré.
Premièrement, isinstance
peut recevoir en deuxième paramètre un tuple de types plutôt qu’un type simple.
Il regardera alors si l’object donné en premier paramètre est une instance de l’un de ces types.
1 2 3 4 5 6 | >>> isinstance(4, (int, str)) True >>> isinstance('foo', (int, str)) True >>> isinstance(['bar'], (int, str)) False |
Ensuite, la méthode __instancecheck__
n’est pas toujours appelée.
Lors d’un appel isinstance(obj, cls)
, la méthode __instancecheck__
est appelée que si type(obj)
n’est pas cls
.
On peut s’en rendre compte avec une classe dont __instancecheck__
renverrait False
pour tout objet testé.
1 2 3 4 5 6 7 8 9 | >>> class NoInstancesMeta(type): ... def __instancecheck__(self, obj): ... return False ... >>> class NoInstances(metaclass=NoInstancesMeta): ... pass ... >>> isinstance(NoInstances(), NoInstances) True |
En revanche, si nous héritons de notre classe NoInstances
:
1 2 3 4 5 | >>> class A(NoInstances): ... pass ... >>> isinstance(A(), NoInstances) False |
Pour comprendre le fonction d’isinstance
, on pourrait grossièrement réécrire l’opérateur avec la fonction suivante.1
1 2 3 4 5 6 | def isinstance(obj, cls): if type(obj) is cls: return True if issubclass(type(cls), tuple): return any(isinstance(obj, c) for c in cls) return type(cls).__instancecheck__(cls, obj) |
-
Voir à ce propos la fonction
PyObject_IsInstance
du fichierObjects/abstract.c
des sources de CPython. ↩
issubclass
Dans la même veine qu’isinstance
, nous avons donc l’opérateur issubclass
, qui vérifie qu’une classe est sous-classe d’une autre.
La surcharge se fait là aussi sur la métaclasse, à l’aide de la méthode spéciale __subclasscheck__
.
Cette méthode est très semblable à __instancecheck__
: en plus de self
(la classe courante), elle reçoit en paramètre la classe à tester.
Elle retourne elle aussi un booléen (True
si la classe donnée est une sous-classe de l’actuelle, False
sinon).
Reprenons ici l’exemple précédent des itérables : notre classe ABCIterable
permet de tester si une classe est un type d’objets itérables.
1 2 3 4 5 6 | class ABCIterableMeta(type): def __subclasscheck__(self, cls): return hasattr(cls, '__iter__') and callable(cls.__iter__) class ABCIterable(metaclass=ABCIterableMeta): pass |
1 2 3 4 5 6 7 8 9 10 11 12 | >>> issubclass(list, ABCIterable) True >>> issubclass(tuple, ABCIterable) True >>> issubclass(str, ABCIterable) True >>> issubclass(dict, ABCIterable) True >>> issubclass(int, ABCIterable) False >>> issubclass(object, ABCIterable) False |
Cet exemple est d’ailleurs meilleur que le précédent, puisque comme Python il vérifie que la méthode __iter__
est présente au niveau de la classe et pas au niveau de l’instance.
Comme isinstance
, issubclass
peut recevoir en deuxième paramètre un tuple de différentes classes à tester.
1 2 3 4 5 6 7 8 9 10 11 | >>> issubclass(int, (int, str)) True >>> issubclass(str, (int, str)) True >>> class Integer(int): ... pass ... >>> issubclass(Integer, (int, str)) True >>> issubclass(list, (int, str)) False |
En revanche, pas de raccourci pour éviter l’appel à __subclasscheck__
, même quand on cherche à vérifier qu’une classe est sa propre sous-classe.
1 2 3 4 5 6 7 8 9 | >>> class NoSubclassesMeta(type): ... def __subclasscheck__(self, cls): ... return False ... >>> class NoSubclasses(metaclass=NoSubclassesMeta): ... pass ... >>> issubclass(NoSubclasses, NoSubclasses) False |
Le cas des classes ABC
Pour les classes abstraites ABC
, c’est-à-dire qui ont abc.ABCMeta
comme métaclasse, une facilité est mise en place.
En effet, ABCMeta
définit une méthode __subclasscheck__
(qui s’occupe entre autres de gérer les classes enregistrées via register
).
Pour éviter de recourir à une nouvelle métaclasse et redéfinir __subclasscheck__
,
la méthode d’ABCMeta
relaie l’appel à la méthode de classe __subclasshook__
, si elle existe.
Ainsi, une classe abstraite n’a qu’à définir __subclasshook__
si elle veut étendre le comportement d’issubclass
.
1 2 3 4 5 6 7 8 9 10 | >>> import abc >>> class ABCIterable(abc.ABC): ... @classmethod ... def __subclasshook__(cls, subcls): ... return hasattr(subcls, '__iter__') and callable(subcls.__iter__) ... >>> issubclass(list, ABCIterable) True >>> issubclass(int, ABCIterable) False |
On notera que la méthode __subclasshook__
sert aussi à l’opérateur isinstance
.
1 2 | >>> isinstance([1, 2, 3], ABCIterable) True |
Contrairement à __subclasscheck__
, __subclasshook__
ne retourne pas forcément un booléen.
Elle peut en effet retourner True
, False
, ou NotImplemented
.
Dans le cas où elle retourne un booléen, il sera la valeur de retour de isinstance
/issubclass
.
Mais dans le cas de NotImplemented
, la main est rendue à la méthode __subclasscheck__
d’ABC
, qui s’occupe de vérifier si les classes sont parentes, ou si la classe est enregistrée (register
).
Nous allons donc réécrire notre classe ABCIterable
de façon à retourner True
si la classe implémente __iter__
, et NotImplemented
sinon.
Ainsi, si la classe hérite d’ABCIterable
mais n’implémente pas __iter__
, elle sera tout de même considérée comme une sous-classe, ce qui n’est pas le cas actuellement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | >>> class ABCIterable(abc.ABC): ... @classmethod ... def __subclasshook__(cls, subcls): ... if hasattr(subcls, '__iter__') and callable(subcls.__iter__): ... return True ... return NotImplemented ... >>> issubclass(list, ABCIterable) True >>> issubclass(int, ABCIterable) False >>> class X(ABCIterable): pass ... >>> issubclass(X, ABCIterable) True |
Collections abstraites
Nous connaissons le module collections
, spécialisé dans les conteneurs ;
et abc
, dédié aux classes abstraites.
Que donnerait le mélange des deux ? collections.abc
!
Ce module fournit des classes abstraites toutes prêtes pour reconnaître les différentes interfaces du langage (Container
, Sequence
, Mapping
, Iterable
, Iterator
, Hashable
, Callable
, etc.).
Assez simples à appréhender, ces classes abstraites testent la présence de méthodes essentielles au respect de l’interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> import collections.abc >>> isinstance(10, collections.abc.Hashable) True >>> isinstance([10], collections.abc.Hashable) False >>> issubclass(list, collections.abc.Sequence) True >>> issubclass(dict, collections.abc.Sequence) False >>> issubclass(list, collections.abc.Mapping) False >>> issubclass(dict, collections.abc.Mapping) True |
Outre la vérification d’interfaces, certaines de ces classes servent aussi de mixins, en apportant des méthodes abstraites et des méthodes prêtes à l’emploi.
La classe MutableMapping
, par exemple, a pour méthodes abstraites __getitem__
, __setitem__
, __delitem__
, __iter__
et __len__
.
Mais la classe fournit en plus l’implémentation d’autres méthodes utiles aux mappings : __contains__
, keys
, items
, values
, get
, __eq__
, __ne__
, pop
, popitem
, clear
, update
, et setdefault
.
C’est-à-dire qu’il suffit de redéfinir les 5 méthodes abstraites pour avoir un type de dictionnaires parfaitement utilisable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class MyMapping(collections.abc.MutableMapping): def __init__(self, *args, **kwargs): super().__init__() self._subdict = dict(*args, **kwargs) def __getitem__(self, key): return self._subdict[key] def __setitem__(self, key, value): self._subdict[key] = value def __delitem__(self, key): del self._subdict[key] def __iter__(self): return iter(self._subdict) def __len__(self): return len(self._subdict) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | >>> m = MyMapping() >>> m['a'] = 0 >>> m['b'] = m['a'] + 1 >>> len(m) 2 >>> list(m.keys()) ['b', 'a'] >>> list(m.values()) [1, 0] >>> dict(m) {'b': 1, 'a': 0} >>> m.get('b') 1 >>> 'a' in m True >>> m.pop('a') 0 >>> 'a' in m False |
Dans un genre similaire, on notera aussi les classes du module numbers
: Number
, Complex
, Real
, Rational
, Integral
.
Ces classes abstraites, en plus de reconnaître l’ensemble des types numériques, permettent par héritage de créer nos propres types de nombres.
TP: Reconnaissance d'interfaces
Je vous propose dans ce TP de nous intéresser à la reconnaissance d’interfaces comme nous l’avons fait dans ce chapitre.
Nous allons premièrement écrire une classe Interface
, héritant de abc.ABC
, qui permettra de vérifier qu’un type implémente un certain nombre de méthodes.
Cette classe sera destinée à être héritée pour spécifier quelles méthodes doivent être implémentées par quels types.
On trouvera par exemple une classe Container
héritant d’Interface
pour vérifier la présence d’une méthode __contains__
.
Les méthodes nécessaires pour se conformer au type seront inscrites dans un attribut de classe __methods__
.
Notre classe Interface
définira la méthode __subclasshook__
pour s’assurer que toutes les méthodes de la séquence __methods__
sont présentes dans la classe.
La méthode __subclasshook__
se déroulera en 3 temps :
- Premièrement, appeler l’implémentation parente via
super
, et retournerFalse
si elle a retournéFalse
. En effet, si la classe parente dit que le type n’est pas un sous-type, on est sûr qu’il n’en est pas un. Mais si la méthode parente retourneTrue
ouNotImplemented
, le doute peut persister ; - Dans un second temps, nous récupérerons la liste de toutes les méthodes à vérifier. Il ne s’agit pas seulement de l’attribut
__methods__
, mais de cet attribut ainsi que celui de toutes les classes parentes ; - Et finalement, nous testerons que chacune des méthodes est présente dans la classe, afin de retourner
True
si elle le sont toutes, etNotImplemented
sinon.
Le deuxième point va nous amener à explorer le MRO, à l’aide de la méthode de classe mro
, et de concaténer les attributs __methods__
de toutes les classes (via la fonction sum
).
Afin de toujours récupérer une séquence, nous utiliserons getattr(cls, '__methods__', ())
, qui nous retournera un tuple vide si l’attribut __methods__
n’est pas présent.
Quant au 3ème point, la builtin all
va nous permettre de vérifier que chaque nom de méthode est présent dans la classe, et qu’il s’agit d’un callable et donc d’une méthode.
Notre classe Interface
peut alors se présenter comme suit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import abc class Interface(abc.ABC): # Attribut `__methods__` vide pour montrer sa structure __methods__ = () @classmethod def __subclasshook__(cls, subcls): # Appel au __subclasshook__ parent ret = super().__subclasshook__(cls, subcls) if not ret: return ret # Récupération de toutes les méthodes all_methods = sum((getattr(c, '__methods__', ()) for c in cls.mro()), ()) # Vérification de la présence des méthodes dans la classe if all(callable(getattr(subcls, meth, None)) for meth in all_methods): return True return NotImplemented |
Nous pouvons dès lors créer nos nouvelles classes hérités d’Interface
avec leurs propres attributs __methods__
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Container(Interface): __methods__ = ('__contains__',) class Sized(Interface): __methods__ = ('__len__',) class SizedContainer(Sized, Container): pass class Subscriptable(Interface): __methods__ = ('__getitem__',) class Iterable(Interface): __methods__ = ('__iter__',) |
Et qui fonctionnent comme prévu.
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> isinstance([], Iterable) True >>> isinstance([], Subscriptable) True >>> isinstance([], SizedContainer) True >>> gen = (x for x in range(10)) >>> isinstance(gen, Iterable) True >>> isinstance(gen, Subscriptable) False >>> isinstance(gen, SizedContainer) False |
Pour terminer, un dernier tour par la documentation et ses pages intéressantes.
- Définition du terme classe abstraite : https://docs.python.org/3/glossary.html#term-abstract-base-class
- Personnaliser la vérification de types : https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks
- Module
abc
: https://docs.python.org/3/library/abc.html - Module
collections.abc
: https://docs.python.org/3/library/collections.abc.html - Module
numbers
: https://docs.python.org/3/library/numbers.html