La propuesta de clases abstractas (bases abstractas) puede encontrarse en el PEP 3119.
Las clases abstractas no pueden instanciarse puesto que poseen una serie de métodos especiales, métodos abstractos que exigen una implementación en una subclase. Están ahí para indicar que esos tipos exigen esos métodos con esos nombres y con esas signaturas.
Los objetivo principales de una clase abstracta son:
- Permitir, a los consumidores de objetos, comprobar la API de los objetos de
forma homogénea, mediante las funciones
isinstance
eissubclass
. - Permitir, a los implementadores de tipos, crear nuevos tipos que se comporten como los definidos en Python.
Un "método mágico" es un método que interactua con los protocolos de Python.
Un protocolo es, intuitivamente, "una manera de hacer las cosas".
-
Trata de obtener la longitud de un número:
len(42)
-
Ahora prueba lo siguiente:
class SizedInt(int): def __len__(self): return len(bin(self)) - 2 answer = SizedInt(42) len(answer)
-
Considera esta clase:
class Rabbit: def __repr__(self): return 'Here is a rabbit!' class Flowers: def __repr__(self): return 'Here are some flowers!' class MagicHat: def get(self, cls): return cls() hat = MagicHat() hat.get(Rabbit) hat.get(Flowers)
¿Qué patrón es este?
Y ahora observa esta versión:
class Rabbit: def __repr__(self): return 'Here is a rabbit!' class Flowers: def __repr__(self): return 'Here are some flowers!' class MagicHat: def __getitem__(self, cls): return cls() hat = MagicHat() hat[Rabbit] hat[Flowers]
Los métodos mágicos permiten definir el comportamiento de cierta sintaxis
de Python, suelen tener la forma __name_of_the_method__
, es decir: dos
guiones bajos, el nombre del método, y otros dos guiones bajos.
Hay decenas de métodos mágicos, que permiten que nuevos tipos se integren más naturalmente con Python y parezcan tipos del lenguaje.
-
¿Cómo podemos saber si un objeto es un contenedor (soporta la sintaxis
obj in conainer
)?Podemos saberlo intentando la operación:
def is_container(something): try: 1 in something # nothing happens, operation supported. return True except TypeError: # operation not supported! return False
O, por introspección comprobando que tiene el método mágico
__contains__
:def is_container(something): return hasattr(something, '__contains__')
La función
hasattr(obj, name)
comprueba siobj
tiene el atributo con nombrename
. El nombre es una cadena de texto.O tratando de ser exhaustivo:
def is_container(something): return isinstance(something, (tuple, list, set, dict))
¿Qué pasa con las cadenas? ¿Funciona con objetos de clases derivadas de lista o tupla? ¿Funciona si una clase personalizada? Por cierto, ¿qué operaciones dirías que definen el tipo "contenedor"?
-
Para responder a estas preguntas, existe el módulo
collections.abc
:from collections.abc import Container assert isinstance([1,2,3], Container) assert isinstance((1,2,3), Container) assert not isinstance(123, Container)
-
Las clases en este módulo redefinen, mediante métodos mágicos, las comprobaciones
isinstance
eissubclass
:class PredicatedBasedContainer: def __init__(self, predicate=lambda _: False): self.predicate = predicate def __contains__(self, item): return bool(self.predicate(item)) empty_set = PredicatedBasedContainer() assert isinstance(empty_set, Container) issubclass(PredicatedBasedContainer, Container)
A
PredicatedBasedContainer
lo llamamos una implementación deContainer
. -
Las clases en este módulo no son, estrictamente, instancias de
type
si no deabc.ABCMeta
:type(Container)
-
El comportamiento de estas clases parece contradecir lo explicado sobre el MRO, en la lección anterior:
Container in PredicatedBasedContainer.__mro__ print(Container.register) assert isinstance(empty_set, Container) hasattr(empty_set, 'register')
Queda claro que
isinstance
tiene que hacer algo más que mirar en la MRO. Algo que depende deContainer
. -
Como decíamos, las clases abstractas no pueden instanciarse:
c = Container()
La implementación de los métodos de las clases abstractas, o lanzan
NotImplementedError
o tienen una implementación inútil (como devolverFalse
siempre, en el caso de__contains__
). -
Las clases en el módulo
numbers
también son clases abstractas, y por tanto instancias deabc.ABCMeta
que forman la torre (jerarquía) numérica.import numbers type(numbers.Integral)
- Documentación de
collections.abc
- Documentación de
numbers
La mayor parte del tiempo, usarás tus propias clases abstractas o las del
módulo collections.abc
para imitar comportamientos de diversos tipos de
colección.
Gran parte del esfuerzo requerido para implementar una clase abstracta viene de saber leer la documentación del módulo. Veamos un par de ejemplos:
ABC | Inherits from | Abstract Methods | Mixin Methods |
---|---|---|---|
Container | __contains__ |
||
... | |||
Set | Collection | __contains__ ,__iter__ ,__len__ |
__le__ , __lt__ , __eq__ , __ne__ __gt__ , __ge__ , __and__ __or__ , __sub__ , __xor__ , isdisjoint |
La tabla muestra el nombre de la clase, luego la clase de la que hereda, luego el listado de métodos abstractos y finalmente los métodos mixin.
Los métodos abstractos son los métodos que una subclase está obligado a implementar.
Los métodos mixin se implementan en base a los métodos abstractos.
-
Nuestro ejemplo
PredicatedBasedContainer
es un ejemplo de especialización (subclassing o subtyping) implícita:from collections.abc import Container class PredicatedBasedContainer: def __init__(self, predicate=lambda _: False): self.predicate = predicate def __contains__(self, item): return bool(self.predicate(item)) ps = PredicatedBasedContainer() isinstance(ps, Container)
Sólo tenemos que proveer del método
__contains__
y, automáticamente, la clase es subclase (y por tanto, sus instancias son también instancias de)Container
.Lo mismo pasa con
Size
. ¿Puedes recuperar la implementación deSizedInt
y comprobar queint
no es subclase deSized
, peroSizedInt
sí lo es? -
Hagamos ahora un
Set
que favorezca el espacio en lugar de la velocidad (Python favorece la velocidad sobre el espacio):from collections.abc import Set class ListBasedSet: ...
Según la documentación de set, tenemos que proporcionar:
__contains__
,__iter__
y__len__
.from collections.abc import Set class ListBasedSet: def __init__(self, iterable=()): self.elements = [] for item in iterable: if item not in self.elements: self.elements.append(item) def __contains__(self, obj): return obj in self.elements def __iter__(self): return iter(self.elements) def __len__(self): return len(self.elements)
-
Cabría esperar, por tanto, que ahora
ListBasedTest
fuera subclase deSet
:issubclass(ListBasedSet, Set)
Pero no lo es.
-
Si queremos que lo sea, tenemos que "registrar" explícitamente
ListBasedTest
como subclase deSet
:Set.register(ListBasedSet) issubclass(ListBasedSet, Set)
-
Al menos ganamos un montón de operaciones entre conjuntos que se basan en esos tres métodos abstractos:
a = ListBasedSet('sdafiohs') b = ListBasedSet('aeiou') vowels = a & b
¿Qué ha pasado?
-
Para utilizar los métodos mixin debemos heredar de esa clase, que es que es donde realmente están los métodos definidos. La clase final queda:
# pay attention to the base classes class ListBasedSet(collections.abc.Set): def __init__(self, iterable=()): self.elements = [] for item in iterable: if item not in self.elements: self.elements.append(item) def __contains__(self, obj): return obj in self.elements def __iter__(self): return iter(self.elements) def __len__(self): return len(self.elements)
Qbserva que, en el momento que heredas de una clase abstracta, ya no necesitas llamar a
register
explícitamente. ¿Por qué?
¿Cómo distinguir entre las clases abstractas que permiten la definición
implícita o explícita? ¡Comprobando los
fuentes de Python
para el módulo collections.abc
!
Las clases que implementen el método de clase mágico __subclasshook__
sobreescriben el funcionamiento de isinstance
e issubclass
. Estas
funciones delegan sobre __subclasshook__
la inspección dinámica de los
atributos que necesitan aparecer en un objeto para ser considerado de
un tipo determinado.
¿Por qué no se proporciona __subclasshook__
en algunas clases? El por qué
oficial viene explicado en la herramienta de seguimiento de bugs de Python:
https://bugs.python.org/issue16728
Esta lección queda un poco confusa por la aparente falta de homogeneidad en la API de las clases abstractas pero recordemos los dos usos principales de las clases abstractas:
- Permitir, a los consumidores de objetos, comprobar la API de los objetos de
forma homogénea, mediante las funciones
isinstance
eissubclass
. - Permitir, a los implementadores de tipos, crear nuevos tipos que se comporten como los definidos en Python.
En general, las ABC pequeñitas, cuyos métodos mixin son ninguno o
vienen de otras ABC, cumplen con el objetivo 1 y la especialización de
nuevas clases es implícita, por duck typing o introspección, encapsulada
en el método __subclasshook__
.
Las ABC que proporcionan muchos métodos mixin que no vienen de otras ABC cumplen el objetivo 2 y la especialización de nuevas clases es explícita puesto que para que puedan proporcionar todos los métodos mixin, el ABC debe ser listado como una base del nuevo tipo.
En cualquier momento podemos insertar, como parte de la jerarquía de ABCs,
un tipo cualquiera, mediante register
.
Además de especializar clases abstractas, podemos crearlas. La forma más
sencilla es heredando de abc.ABC
.
-
En el marco de un framework de juegos, crea las siguientes clases abstractas:
from abc import ABC, abstractmethod class Kill(ABC): @abstractmethod def kill(self): raise NotImplementedError class Visible(ABC): @abstractmethod def set_visible(self, visibility=True): raise NotImplementedError @abstractmethod def is_visible(self): return False # mixin methods: def show(self): self.set_visible(True) def hide(self): self.set_visible(False) class Update(ABC): @abstractmethod def update(self, t, dt): raise NotImplementedError
-
Comprueba que las nuevas clases son clases abstractas:
type(Update)
-
Podemos crear ahora una entidad de juego que herede de alguna combinación de ellas:
class Enemy(Entity, Kill, Visible, Update): ... e = Enemy()
Fíjate bien en el error: aun no podemos instanciarla porque tiene métodos abstractos.
-
Implementa los métodos abstractos:
class Enemy(Entity, Kill, Visible, Update): def __init__(self): self._visible = False def kill(self): print('Arrrrg, I die!') def is_visible(self): return self._visible def set_visible(self, visibility): self._visible = visibility def update(self, t, dt): print('Looking for the character...')
-
Ahora podemos instanciarla y comprobar que es una instancia de todas las clases:
e = Enemy() assert isinstance(e, Update) assert isinstance(e, Visible) assert isinstance(e, Kill)
-
Habilita la especialización implícita usando el método de clase mágico
__subclasshook__
:class Kill(ABC): @abstractmethod def kill(self): raise NotImplementedError @classmethod def __subclasshook__(cls, C): return _check_methods(C, 'kill') class Visible(ABC): @abstractmethod def set_visible(self, visibility=True): raise NotImplementedError @abstractmethod def is_visible(self): return False # mixin methods: def show(self): self.set_visible(True) def hide(self): self.set_visible(False) @classmethod def __subclasshook__(cls, C): return _check_methods(C, 'set_visible', 'is_visible') class Update(ABC): @abstractmethod def update(self, t, dt): raise NotImplementedError @classmethod def __subclasshook__(cls, C): return _check_methods(C, 'update') def _check_methods(C, *methods): mro = C.__mro__ for method in methods: for B in mro: if method in B.__dict__: if B.__dict__[method] is None: return NotImplemented break else: return NotImplemented return True
-
Ahora crea un tipo que no herede de enemigo pero que parezca un enemigo:
class Blob: def __init__(self): self._visible = False def kill(self): print('blaaaarg!') def is_visible(self): return self._visible def set_visible(self, visibility): self._visible = visibility def update(self, t, dt): print('blup blup blup') theblob = Blob() assert isinstance(theblob, Kill) assert isinstance(theblob, Update) assert isinstance(theblob, Visible) assert not isinstance(theblob, Enemy)
-
Comprueba que es un tipo de enemigo:
isinstance(theblob, Enemy)
-
¿Cómo lo solucionarías?