web-dev-qa-db-fra.com

En utilisant abc.ABCMeta d'une manière, il est compatible à la fois avec Python 2.7 et Python 3.5

Je voudrais créer une classe qui a abc.ABCMeta en tant que métaclasse et est compatible à la fois avec Python 2.7 et Python 3.5. Jusqu'à présent, je n'ai réussi à le faire que sur 2.7 ou sur 3.5 - mais jamais sur les deux versions simultanément. Quelqu'un pourrait-il m'aider?

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

Essai

Si nous exécutons le test suivant en utilisant la version appropriée de l'interpréteur Python (Python 2.7 -> Exemple 1, Python 3.5 -> Exemple 2), cela réussit dans les deux scénarios:

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)

Problème

Lors de l'exécution du test à l'aide de Python 3.5, le comportement attendu ne se produit pas (TypeError n'est pas déclenché lors de l'instanciation 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

----------------------------------------------------------------------

Alors que l'exécution du test en utilisant Python 2.7 soulève un SyntaxError:

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

Vous pouvez utiliser six.add_metaclass ou six.with_metaclass :

import abc, six

@six.add_metaclass(abc.ABCMeta)
class SomeAbstractClass():
    @abc.abstractmethod
    def do_something(self):
        pass

six est une bibliothèque de compatibilité Python 2 et 3 . Vous pouvez l'installer en exécutant pip install six ou en téléchargeant la dernière version de six.py dans le répertoire de votre projet.

Pour ceux d'entre vous qui préfèrent future à six, la fonction appropriée est future.utils.with_metaclass .

52
vaultah

Utilisant abc.ABCMeta d'une manière compatible avec Python 2.7 et Python 3.5

Si nous n'utilisions que Python 3 (c'est nouveau dans 3.4 ) nous pourrions faire:

from abc import ABC

et hérite de ABC au lieu de object. C'est:

class SomeAbstractClass(ABC):
    ...etc

Vous n'avez toujours pas besoin d'une dépendance supplémentaire (le module six) - vous pouvez utiliser la métaclasse pour créer un parent (c'est essentiellement ce que fait le module six avec 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

Ou vous pouvez simplement le faire sur place (mais c'est plus compliqué et ne contribue pas autant à la réutilisation):

# use ABCMeta compatible with Python 2 *and* 3 
class SomeAbstractClass(abc.ABCMeta('ABC', (object,), {'__slots__': ()})):

    @abc.abstractmethod
    def do_something(self):
        pass

Notez que la signature semble un peu plus compliquée que six.with_metaclass mais c'est sensiblement la même sémantique, sans la dépendance supplémentaire.

Soit solution

et maintenant, quand nous essayons d'instancier sans implémenter l'abstraction, nous obtenons précisément ce que nous attendons:

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

Remarque sur __slots__ = ()

Nous juste ajouté vide __slots__ à la classe de commodité ABC dans Python 3 bibliothèque standard, et ma réponse est mise à jour pour l'inclure.

Ne pas avoir __dict__ Et __weakref__ Disponibles dans le parent ABC permet aux utilisateurs de refuser leur création pour les classes enfants et d'économiser de la mémoire - il n'y a pas d'inconvénients, sauf si vous utilisiez __slots__ Dans les classes enfants déjà et en s'appuyant sur la création implicite __dict__ Ou __weakref__ À partir du parent ABC.

La solution rapide serait de déclarer __dict__ Ou __weakref__ Dans votre classe enfant selon le cas. Mieux (pour __dict__) Pourrait être de déclarer explicitement tous vos membres.

30
Aaron Hall

Je préfère réponse d'Aaron Hall , mais il est important de noter que dans ce cas, le commentaire qui fait partie de la ligne:

ABC = abc.ABCMeta('ABC', (object,), {}) # compatible with Python 2 *and* 3 

... est tout aussi important que le code lui-même. Sans le commentaire, rien n'empêche un futur cow-boy de supprimer la ligne et de changer l'héritage de classe en:

class SomeAbstractClass(abc.ABC):

... cassant ainsi tout pré Python 3.4.

Un Tweak qui peut être un peu plus explicite/clair pour quelqu'un d'autre - en ce qu'il est auto-documenté - concernant ce que vous essayez d'accomplir:

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

À strictement parler, cela n'est pas nécessaire, mais il est absolument clair, même sans commentaire, ce qui se passe.

15
Rick Teachey

Juste pour dire que vous devez explicitement passer str('ABC') à abc.ABCMeta Dans Python 2 si vous utilisez from __future__ import unicode_literals.

Sinon Python lève TypeError: type() argument 1 must be string, not unicode.

Voir le code corrigé ci-dessous.

import sys
import abc
from __future__ import unicode_literals

if sys.version_info >= (3, 4):
    ABC = abc.ABC
else:
    ABC = abc.ABCMeta(str('ABC'), (), {})

Cela ne nécessiterait pas une réponse distincte mais malheureusement je ne peux pas commenter la vôtre (besoin de plus de représentants).

4
FabienP