【问题标题】:Using abc.ABCMeta in a way it is compatible both with Python 2.7 and Python 3.5以与 Python 2.7 和 Python 3.5 兼容的方式使用 abc.ABCMeta
【发布时间】:2016-06-10 22:52:05
【问题描述】:

我想创建一个将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

【问题讨论】:

    标签: python python-2.7 metaclass python-3.5 abc


    【解决方案1】:

    您可以使用six.add_metaclasssix.with_metaclass

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

    six 是一个Python 2 and 3 compatibility library。您可以通过运行pip install six 或将最新版本的six.py 下载到您的项目目录来安装它。

    对于那些更喜欢future 而不是six 的人,相关功能是future.utils.with_metaclass

    【讨论】:

    • fyi,future.utils 的语法是class Test(with_metaclass(abc.ABCMeta, object)):
    • 我只是想知道,一旦项目完全迁移到 python3,清理这六个/未来的烂摊子会有多困难。
    • @LokeshAgrawal - 尝试使用 Yelp 编写的 undebt 包!见engineeringblog.yelp.com/2016/08/…
    【解决方案2】:

    以与 Python 2.7 和 Python 3.5 兼容的方式使用 abc.ABCMeta

    如果我们只使用 Python 3(这是new in 3.4),我们可以这样做:

    from abc import ABC
    

    并继承自 ABC 而不是 object。那就是:

    class SomeAbstractClass(ABC):
        ...etc
    

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

    我们 just added empty __slots__ 到 Python 3 标准库中的 ABC 便利类,我的答案已更新以包含它。

    ABC 父级中没有 __dict____weakref__ 可用,这允许用户拒绝为子类创建并节省内存 - 没有缺点,除非您已经在子类中使用 __slots__ 并且依赖从 ABC 父级创建隐式 __dict____weakref__

    快速解决方法是在您的子类中酌情声明__dict____weakref__。更好的(对于__dict__)可能是明确声明所有成员。

    【讨论】:

    • 在这里添加空的__slots__很好,我前几天刚用过!
    【解决方案3】:

    只是说如果你使用from __future__ import unicode_literals,你必须在Python 2中显式地将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'
    【解决方案4】:

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

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

    ...与代码本身一样重要。没有评论,没有什么可以阻止未来的牛仔删除该行并将类继承更改为:

    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
    

    严格来说,这不是必须的,但绝对清楚,即使没有评论,发生了什么。

    【讨论】:

    • 您可以更简洁地比较版本:sys.version_info &gt;= (3, 4)
    • 我喜欢明确的版本检查,因为它太容易错过/忽略 cmets。
    • 您绝对应该使用@zondo 评论中的版本并将version_info 与元组进行比较。在 python 4.1 上单独比较 version_info[0]version_info[1] 会中断
    • @TheEspinosa 我之前因为懒惰而将其保留,但我现在已更改它,因为您是正确的:原始代码已损坏,例如未来的 4.1 版本的 Python。
    猜你喜欢
    • 2020-05-21
    • 2018-09-16
    • 2012-02-11
    • 2019-02-06
    • 2015-12-23
    • 2017-06-28
    • 1970-01-01
    • 2016-01-02
    • 1970-01-01
    相关资源
    最近更新 更多