Usar abc.ABCMeta de una manera que sea compatible con Python 2.7 y Python 3.5

Me gustaría crear una clase que tenga abc.ABCMeta como metaclase y sea compatible con Python 2.7 y Python 3.5. Hasta ahora, solo logré hacer esto en 2.7 o en 3.5, pero nunca en ambas versiones simultáneamente. ¿Podría alguien echarme 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), tendrá é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

Mientras se ejecuta la prueba con Python 3.5, el comportamiento esperado no se produce ( TypeError no se SomeAbstractClass crear SomeAbstractClass instancia de 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 genera un SyntaxError :

  Python 2.7 incompatible Raises exception: File "/home/tati/sample_abc.py", line 24 class SomeAbstractClass(metaclass=abc.ABCMeta): ^ SyntaxError: invalid syntax 

Puedes 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 de Python 2 y 3 . Puede instalarlo ejecutando pip install six o descargando la última versión de six.py en el directorio de su proyecto.

Para aquellos de ustedes que prefieren el future sobre los six , la función relevante es future.utils.with_metaclass .

Usar abc.ABCMeta de una manera que sea compatible con Python 2.7 y Python 3.5

Si solo estuviéramos usando 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 

Aún no necesita una dependencia adicional (el módulo seis); puede usar la metaclase para crear un padre (esto es esencialmente lo que hace el módulo 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 desordenado y no contribuye tanto a la reutilización):

 # 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 dos soluciones

y ahora, cuando intentamos crear instancias sin implementar la abstracción, obtenemos exactamente lo que esperamos:

 >>> SomeAbstractClass() Traceback (most recent call last): File "", line 1, in  SomeAbstractClass() TypeError: Can't instantiate abstract class SomeAbstractClass with abstract methods do_something 

Nota sobre __slots__ = ()

Acabamos de agregar __slots__ vacíos a la clase de conveniencia ABC en la biblioteca estándar de Python 3, y mi respuesta se actualizó para incluirla.

No tener __dict__ y __weakref__ disponibles en ABC parent le permite a los usuarios negar su creación para clases secundarias y ahorrar memoria; no hay inconvenientes, a menos que ya estuviera usando __slots__ en clases secundarias y confíe en la __dict__ implícita de __dict__ o __weakref__ del padre ABC .

La solución rápida sería declarar __dict__ o __weakref__ en la clase de su hijo, 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 propio código. Sin el comentario, no hay nada que impida que algún futuro vaquero en el futuro elimine la línea y cambie la herencia de clase a:

 class SomeAbstractClass(abc.ABC): 

… rompiendo así todo lo pre Python 3.4.

Un truco 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 hacerlo, pero es absolutamente claro, incluso sin comentarios, lo que está sucediendo.

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 genera TypeError: type() argument 1 must be string, not unicode .

Ver 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 la suya (necesito más representantes).