为什么 __slots__ 从抽象基础 class 继承时在 Python 2 和 3 中表现不同

Why is __slots__ behaving differently in Python 2 and 3 when inheriting from an abstract base class

我创建了以下 class 以节省内存的方式在平面上存储可变点 - 我需要 namedtuple('Point', 'x y') 的可变等价物。由于实例词典很大,我想我会选择 __slots__:

from collections import Sequence

class Point(Sequence):
    __slots__ = ('x', 'y')

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __getitem__(self, item):
        return getattr(self, self.__slots__[item])

    def __setitem__(self, item, value):
        return setattr(self, self.__slots__[item], value)

    def __repr__(self):
        return 'Point(x=%r, y=%r)' % (self.x, self.y)

    def __len__(self):
        return 2

在 Python 3 上测试时,似乎一切正常:

>>> pt = Point(12, 42)
>>> pt[0], pt.y
(12, 42)
>>> pt.x = 5
>>> pt
Point(x=5, y=42)
>>> pt.z = 6
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'

但是在 Python 2 上,我可以设置属性 z 即使它不在插槽中:

>>> pt = Point(12, 42)
>>> pt.z = 5
>>> pt.z
5
>>> pt.__slots__
('x', 'y')
>>> pt.__dict__
{'z': 5}

为什么会这样,为什么 Python 2 和 Python 3 有区别?

Python 2 数据模型在 __slots__ 上表示如下:

  • When inheriting from a class without __slots__, the __dict__ attribute of that class will always be accessible, so a __slots__ definition in the subclass is meaningless.

这就是这里发生的事情。在 Python 2 中,collections 模块中的抽象基础 classes 根本没有 __slots__

>>> from collections import Sequence
>>> Sequence.__slots__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Sequence' has no attribute '__slots__'

这在 CPython 问题跟踪器中被报告为 issue 11333,并已在 Python 3.3 中修复。

在 Python 3.3+ 中,Sequence 抽象基础 class 现在 __slots__ 设置为一个空元组:

>>> from collections import Sequence
>>> Sequence.__slots__
()

因此在 Python 2 中,您不能从 collections 基础 class 继承并同时具有 __slots__ 的内存高效存储。


但是请注意,尽管 collections abstract base classes 上的文档声称

These ABCs allow us to ask classes or instances if they provide particular functionality, for example:

size = None
if isinstance(myvar, collections.Sized):
    size = len(myvar)

This is not the case with Sequence;简单地实现 Sequence 所需的所有方法并不能使您的 class 实例通过 isinstance 检查。

原因是 Sequence class does not have a __subclasshook__; and in its absence, the parent class __subclasshook__ is consulted instead; in this case Sized.__subclasshook__;并且 returns NotImplemented 如果针对 class 的测试不是 完全 Sized.

另一方面,无法通过魔术方法区分映射类型和序列类型,因为它们都可以具有完全相同的魔术方法 - collections.OrderedDict所有一个Sequence的魔术方法,包括__reversed__方法,但它不是一个序列。

但是,你仍然不需要继承Sequence来制作isinstance(Point, Sequence)returnTrue。在下面的示例中,Point 是相同的,除了派生自 object 而不是 Sequence,在 Python 2:

>>> pt = Point(12, 42)
>>> pt.z = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'
>>> isinstance(pt, Sequence)
False
>>> Sequence.register(pt)
>>> isinstance(pt, Sequence)
True

您可以将任何 class 注册为抽象基础 class 的子 class 以用于 isinstance 检查;在额外的混合方法中,你真的只需要实现 countindex;其他人的功能将由 Python 运行时填充。