create - python interface
Usando abc.ABCMeta de una manera es compatible tanto con Python 2.7 como con Python 3.5 (4)
Me gustaría crear una clase que tenga abc.ABCMeta
como una metaclase y sea compatible tanto con Python 2.7 como con Python 3.5. Hasta ahora, solo lo conseguí en 2.7 o en 3.5, pero nunca en ambas versiones simultáneamente. ¿Podría alguien darme una mano?
Python 2.7:
import abc
class SomeAbstractClass(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def do_something(self):
pass
Python 3.5:
import abc
class SomeAbstractClass(metaclass=abc.ABCMeta):
@abc.abstractmethod
def do_something(self):
pass
Pruebas
Si ejecutamos la siguiente prueba utilizando la versión adecuada del intérprete de Python (Python 2.7 -> Ejemplo 1, Python 3.5 -> Ejemplo 2), tiene éxito en ambos escenarios:
import unittest
class SomeAbstractClassTestCase(unittest.TestCase):
def test_do_something_raises_exception(self):
with self.assertRaises(TypeError) as error:
processor = SomeAbstractClass()
msg = str(error.exception)
expected_msg = "Can''t instantiate abstract class SomeAbstractClass with abstract methods do_something"
self.assertEqual(msg, expected_msg)
Problema
Al ejecutar la prueba con Python 3.5, el comportamiento esperado no ocurre ( TypeError
no se SomeAbstractClass
instanciar SomeAbstractClass
):
======================================================================
FAIL: test_do_something_raises_exception (__main__.SomeAbstractClassTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/tati/sample_abc.py", line 22, in test_do_something_raises_exception
processor = SomeAbstractClass()
AssertionError: TypeError not raised
----------------------------------------------------------------------
Mientras que ejecutar la prueba usando Python 2.7 plantea un SyntaxError
:
Python 2.7 incompatible
Raises exception:
File "/home/tati/sample_abc.py", line 24
class SomeAbstractClass(metaclass=abc.ABCMeta):
^
SyntaxError: invalid syntax
Usando abc.ABCMeta de una manera es compatible tanto con Python 2.7 como con Python 3.5
Si solo utilizáramos Python 3 (esto es nuevo en 3.4 ) podríamos hacer:
from abc import ABC
y heredar de ABC
lugar de object
. Es decir:
class SomeAbstractClass(ABC):
...etc
Todavía no necesita una dependencia adicional (el módulo de seis): puede usar la metaclase para crear un elemento primario (esto es esencialmente lo que hace el módulo de seis en with_metaclass):
import abc
# compatible with Python 2 *and* 3:
ABC = abc.ABCMeta(''ABC'', (object,), {''__slots__'': ()})
class SomeAbstractClass(ABC):
@abc.abstractmethod
def do_something(self):
pass
O simplemente puede hacerlo en el lugar (pero esto es más complicado y no contribuye tanto como para volver a usarlo):
# use ABCMeta compatible with Python 2 *and* 3
class SomeAbstractClass(abc.ABCMeta(''ABC'', (object,), {''__slots__'': ()})):
@abc.abstractmethod
def do_something(self):
pass
Tenga en cuenta que la firma parece un poco más desordenada que six.with_metaclass
pero es sustancialmente la misma semántica, sin la dependencia adicional.
Cualquiera de las soluciones
y ahora, cuando intentamos crear instancias sin implementar la abstracción, obtenemos exactamente lo que esperamos:
>>> SomeAbstractClass()
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
SomeAbstractClass()
TypeError: Can''t instantiate abstract class SomeAbstractClass with abstract methods do_something
Nota sobre __slots__ = ()
Acabamos de agregar __slots__
vacías a la clase de conveniencia ABC en la biblioteca estándar de Python 3, y mi respuesta se actualiza para incluirla.
No tener __dict__
y __weakref__
disponibles en el padre ABC
permite a los usuarios denegar su creación para clases secundarias y guardar memoria; no hay inconvenientes, a menos que estuviera usando __slots__
en clases secundarias y confiando en la __dict__
implícita __dict__
o __weakref__
del padre ABC
.
La solución rápida sería declarar __dict__
o __weakref__
en su clase secundaria, según corresponda. Mejor (para __dict__
) podría ser declarar explícitamente a todos sus miembros.
Prefiero la respuesta de Aaron Hall , pero es importante tener en cuenta que en este caso el comentario es parte de la línea:
ABC = abc.ABCMeta(''ABC'', (object,), {}) # compatible with Python 2 *and* 3
... es tan importante como el código en sí. Sin el comentario, no hay nada que evite que algún futuro vaquero sea eliminado de la lista y cambie la herencia de la clase a:
class SomeAbstractClass(abc.ABC):
... rompiendo todo antes de Python 3.4.
Un ajuste que puede ser un poco más explícito / claro para otra persona, en el sentido de que se documenta a sí mismo, con respecto a lo que está tratando de lograr:
import sys
import abc
if sys.version_info >= (3, 4):
ABC = abc.ABC
else:
ABC = abc.ABCMeta(''ABC'', (), {})
class SomeAbstractClass(ABC):
@abc.abstractmethod
def do_something(self):
pass
Estrictamente hablando, esto no es necesario, pero está absolutamente claro, incluso sin comentarios, lo que está sucediendo.
Puede usar six.add_metaclass
o six.with_metaclass
:
import abc, six
@six.add_metaclass(abc.ABCMeta)
class SomeAbstractClass():
@abc.abstractmethod
def do_something(self):
pass
six
es una biblioteca de compatibilidad Python 2 y 3 . Puede instalarlo ejecutando pip install six
o descargando la última versión de six.py
al directorio de su proyecto.
Para aquellos de ustedes que prefieren el future
sobre six
, la función relevante es future.utils.with_metaclass
.
Solo para decir que debe pasar explícitamente str(''ABC'')
a abc.ABCMeta
en Python 2 si usa from __future__ import unicode_literals
.
De lo contrario, Python plantea TypeError: type() argument 1 must be string, not unicode
.
Vea el código corregido a continuación.
import sys
import abc
from __future__ import unicode_literals
if sys.version_info >= (3, 4):
ABC = abc.ABC
else:
ABC = abc.ABCMeta(str(''ABC''), (), {})
Esto no requeriría una respuesta por separado, pero lamentablemente no puedo comentar el suyo (necesito más representantes).