如何在Python 2.7和Python 3.5兼容的情况下使用abc.ABCMeta?

60
我想创建一个类,它的元类是abc.ABCMeta,并且可以同时兼容Python 2.7和Python 3.5。到目前为止,我只能在2.7或3.5上成功创建,但从未在两个版本上同时成功。有人可以帮我吗?
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

测试

如果我们使用适当版本的Python解释器(Python 2.7 -> 示例1,Python 3.5 -> 示例2)运行以下测试,两种情况都会成功:

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)

问题

在使用Python 3.5运行测试时,期望的行为没有发生(即在实例化SomeAbstractClass时未引发TypeError):

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

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

当使用Python 2.7运行测试时,会引起SyntaxError错误:

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

67

1
FYI,future.utils的语法是class Test(with_metaclass(abc.ABCMeta, object)): - sam-6174
3
我只是想知道,当这个项目完全迁移到Python3后,清理这一堆混乱会有多难。 - ThinkGeek
1
@LokeshAgrawal - 尝试使用 Yelp 编写的 "undebt" 包!请参见 https://engineeringblog.yelp.com/2016/08/undebt-how-we-refactored-3-million-lines-of-code.html 。 - PaulMcG

39

如何在Python 2.7和Python 3.5中兼容使用abc.ABCMeta

如果我们只使用Python 3(这是3.4中的新功能),我们可以这样做:

from abc import ABC

而是继承自ABC,而不是object。也就是说:
class SomeAbstractClass(ABC):
    ...etc

你仍然不需要额外的依赖(six模块)- 你可以使用元类来创建一个父类(这本质上就是six模块在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

或者你可以直接在原地完成它(但这样会更加混乱,并且对重用的贡献不如前一种方法):

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

    @abc.abstractmethod
    def do_something(self):
        pass

请注意,签名看起来比 six.with_metaclass 更混乱,但它实际上具有基本相同的语义,而不需要额外的依赖。

任一解决方案

现在,当我们尝试在没有实现抽象的情况下进行实例化时,我们得到了我们预期的结果。
>>> 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

关于__slots__ = ()的说明

在Python 3标准库中的ABC方便类中,我们刚刚添加了空的__slots__,我的答案已经更新以包括它。

ABC父类中没有可用的__dict____weakref__可以让用户拒绝为子类创建它们并节省内存-除非您已经在子类中使用__slots__并依赖于从ABC父类自动创建的隐式__dict____weakref__

快速修复方法是在您的子类中声明适当的__dict____weakref__。更好的方法(对于__dict__)可能是显式声明所有成员。


在这里添加空的 __slots__ 是个不错的选择,我就是前几天刚用到它! - Rick

20

我更喜欢Aaron Hall的回答,但重要的是要注意,在这种情况下,注释是该行的一部分:

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

...因此破坏了 Python 3.4 之前的所有内容。

可能会有一个更明确/清晰的调整方法对于其他人来说更加自说明- 关于你试图实现什么的自我记录:

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

严格来说,这并不是必须要做的,但即使没有评论,发生的事情也非常清楚。


3
可以更简洁地比较版本:sys.version_info >= (3, 4) - zondo
我喜欢显式版本检查,因为很容易忽略/忽视注释。 - Griffin
1
你应该绝对采用@zondo评论中的版本,并将version_info与元组进行比较。独立比较version_info[0]version_info[1]将在Python 4.1上出现错误。 - Bernhard
@TheEspinosa,之前因为懒惰我没有改变它,但现在我已经改变了,因为你是正确的:原始代码对于未来的Python 4.1版本是有问题的。 - Rick

5

需要说明的是,如果您在Python 2中使用 from __future__ import unicode_literals,则必须明确地将str('ABC')传递给abc.ABCMeta

否则,Python会引发TypeError: type() argument 1 must be string, not unicode错误。

请参见以下已更正代码。

import sys
import abc
from __future__ import unicode_literals

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

这不需要单独回答,但很遗憾我无法评论你的回答(需要更多声望)。

或者直接使用 b'ABC' - Eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接