问题
I'd like to create a class which has abc.ABCMeta
as a metaclass and is compatible both with Python 2.7 and Python 3.5. Until now, I only succeeded doing this either on 2.7 or on 3.5 - but never on both versions simultaneously. Could someone give me a hand?
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
Testing
If we run the following test using the suitable version of the Python interpreter (Python 2.7 -> Example 1, Python 3.5 -> Example 2), it succeeds in both scenarios:
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)
Problem
While running the test using Python 3.5, the expected behavior doesn't happen (TypeError
is not raised while instantiating 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
----------------------------------------------------------------------
Whereas running the test using Python 2.7 raises a SyntaxError
:
Python 2.7 incompatible
Raises exception:
File "/home/tati/sample_abc.py", line 24
class SomeAbstractClass(metaclass=abc.ABCMeta):
^
SyntaxError: invalid syntax
回答1:
You could use six.add_metaclass or six.with_metaclass:
import abc, six
@six.add_metaclass(abc.ABCMeta)
class SomeAbstractClass():
@abc.abstractmethod
def do_something(self):
pass
six
is a Python 2 and 3 compatibility library. You can install it by running pip install six
or by downloading the latest version of six.py
to your project directory.
For those of you who prefer future
over six
, the relevant function is future.utils.with_metaclass.
回答2:
Using abc.ABCMeta in a way it is compatible both with Python 2.7 and Python 3.5
If we were only using Python 3 (this is new in 3.4) we could do:
from abc import ABC
and inherit from ABC
instead of object
. That is:
class SomeAbstractClass(ABC):
...etc
You still don't need an extra dependence (the six module) - you can use the metaclass to create a parent (this is essentially what the six module does in 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
Or you could just do it in-place (but this is more messy, and doesn't contribute as much to reuse):
# use ABCMeta compatible with Python 2 *and* 3
class SomeAbstractClass(abc.ABCMeta('ABC', (object,), {'__slots__': ()})):
@abc.abstractmethod
def do_something(self):
pass
Note that the signature looks a little messier than six.with_metaclass but it is substantially the same semantics, without the extra dependence.
Either solution
and now, when we try to instantiate without implementing the abstraction, we get precisely what we expect:
>>> 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
Note on __slots__ = ()
We just added empty __slots__ to the ABC convenience class in Python 3's standard library, and my answer is updated to include it.
Not having __dict__
and __weakref__
available in the ABC
parent allows users to deny their creation for child classes and save memory - there are no downsides, unless you were using __slots__
in child classes already and relying on implicit __dict__
or __weakref__
creation from the ABC
parent.
The fast fix would be to declare __dict__
or __weakref__
in your child class as appropriate. Better (for __dict__
) might be to declare all your members explicitly.
回答3:
I prefer Aaron Hall's answer, but it's important to note that in this case the comment that is part of the line:
ABC = abc.ABCMeta('ABC', (object,), {}) # compatible with Python 2 *and* 3
...is every bit as important as the code itself. Without the comment, there is nothing to prevent some future cowboy down the road deleting the line and changing the class inheritance to:
class SomeAbstractClass(abc.ABC):
...thus breaking everything pre Python 3.4.
One tweak that may be a little more explicit/clear to someone else- in that it is self documenting- regarding what it is you are trying to accomplish:
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
Strictly speaking, this isn't necessary to do, but it is absolutely clear, even without commentary, what is going on.
回答4:
Just to say that you must explicitly pass str('ABC')
to abc.ABCMeta
in Python 2 if you use from __future__ import unicode_literals
.
Otherwise Python raises TypeError: type() argument 1 must be string, not unicode
.
See corrected code below.
import sys
import abc
from __future__ import unicode_literals
if sys.version_info >= (3, 4):
ABC = abc.ABC
else:
ABC = abc.ABCMeta(str('ABC'), (), {})
This would not require a separate answer but sadly I cannot comment yours (need more rep).
来源:https://stackoverflow.com/questions/35673474/using-abc-abcmeta-in-a-way-it-is-compatible-both-with-python-2-7-and-python-3-5