Skip to content

Latest commit

 

History

History
560 lines (409 loc) · 15.9 KB

File metadata and controls

560 lines (409 loc) · 15.9 KB

Métodos mágicos y clases base abstractas

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:

  1. Permitir, a los consumidores de objetos, comprobar la API de los objetos de forma homogénea, mediante las funciones isinstance e issubclass.
  2. Permitir, a los implementadores de tipos, crear nuevos tipos que se comporten como los definidos en Python.

Métodos mágicos

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".

  1. Trata de obtener la longitud de un número:

    len(42)
  2. Ahora prueba lo siguiente:

    class SizedInt(int):
        def __len__(self):
            return len(bin(self)) - 2
    
    answer = SizedInt(42)
    len(answer)
  3. 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.

Clases abstractas en Python

  1. ¿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 si obj tiene el atributo con nombre name. 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"?

  2. 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)
  3. Las clases en este módulo redefinen, mediante métodos mágicos, las comprobaciones isinstance e issubclass:

    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 de Container.

  4. Las clases en este módulo no son, estrictamente, instancias de type si no de abc.ABCMeta:

    type(Container)
  5. 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 de Container.

  6. 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 devolver False siempre, en el caso de __contains__).

  7. Las clases en el módulo numbers también son clases abstractas, y por tanto instancias de abc.ABCMeta que forman la torre (jerarquía) numérica.

    import numbers
    type(numbers.Integral)

Especialización de clases abstractas

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.

  1. 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 de SizedInt y comprobar que int no es subclase de Sized, pero SizedInt sí lo es?

  2. 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)
  3. Cabría esperar, por tanto, que ahora ListBasedTest fuera subclase de Set:

    issubclass(ListBasedSet, Set)

    Pero no lo es.

  4. Si queremos que lo sea, tenemos que "registrar" explícitamente ListBasedTest como subclase de Set:

    Set.register(ListBasedSet)
    issubclass(ListBasedSet, Set)
  5. 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?

  6. 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é?

Sobre la especialización explícita e implícita

¿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

Conclusiones

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:

  1. Permitir, a los consumidores de objetos, comprobar la API de los objetos de forma homogénea, mediante las funciones isinstance e issubclass.
  2. 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.

Creando nuevas clases abstractas

Además de especializar clases abstractas, podemos crearlas. La forma más sencilla es heredando de abc.ABC.

  1. 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
  2. Comprueba que las nuevas clases son clases abstractas:

    type(Update)
  3. 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.

  4. 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...')
  5. 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)
  6. 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
  7. 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)
  8. Comprueba que es un tipo de enemigo:

    isinstance(theblob, Enemy)
  9. ¿Cómo lo solucionarías?