是否可以在已经定义 __slots__ 的类的装饰器中添加 __slots__ ?

Nar*_*lei 3 python metaclass slots

首先我要说的是,我了解槽和元类在 Python 中的工作原理。在处理这两个问题时,我遇到了一个有趣的问题。这是一个最小的例子:

def decorator(cls):
    dct = dict(cls.__dict__)
    dct['__slots__'] = ('y',)
    return type('NewClass', cls.__bases__, dct)

@decorator
class A(object):
    __slots__= ('x',)
    def __init__(self):
        self.x = 'xx'

A()
Run Code Online (Sandbox Code Playgroud)

这会产生以下异常:

Traceback (most recent call last):
  File "p.py", line 12, in <module>
    A()
  File "p.py", line 10, in __init__
    self.x = 'xx'
TypeError: descriptor 'x' for 'A' objects doesn't apply to 'NewClass' object
Run Code Online (Sandbox Code Playgroud)

现在,我知道为什么会发生这种情况:为槽创建的描述符x必须能够引用槽的保留空间。只有类 A 的实例或 A 的子类的实例才有此保留空间,因此只有这些实例可以使用描述符x。在上面的示例中,元类创建了一个新类型,它是 A 基类的子类,但不是 A 本身的子类,因此我们得到了异常。够简单的。

当然,在这个简单的示例中,以下两个定义之一都decorator可以解决该问题:

def decorator(cls):
    dct = dict(cls.__dict__)
    dct['__slots__'] = ('y',)
    return type('NewClass', (cls,) + cls.__bases__, dct)

def decorator(cls):
    class NewClass(cls):
        __slots__ = ('y',)
    return NewClass
Run Code Online (Sandbox Code Playgroud)

但这些解决方法与原始方法并不完全相同,因为它们都添加了 A 作为基类。他们可能会在更复杂的设置中失败。例如,如果继承树比较复杂,您可能会遇到以下异常:TypeError: multiple bases have instance lay-out conflict

所以我的非常具体的问题是:

有没有办法通过调用创建一个新类,type修改__slots__现有类的属性,但不将现有类添加为新类的基类?

编辑:

我知道严格的元类是我上面的示例的另一种解决方法。有很多方法可以使最小的示例发挥作用,但我的问题是通过new基于现有类的方式创建一个类,而不是如何使示例发挥作用。对困惑感到抱歉。

编辑2:

评论中的讨论让我提出了比我最初问的更精确的问题:

是否可以通过调用创建一个类,type该类使用现有类的槽和描述符,而不是该类的后代?

如果答案是“否”,我希望有消息人士告诉我为什么不这样做。

1st*_*st1 5

__slots__不,不幸的是,在创建类之后(也就是调用它们上的装饰器时),无法对 执行任何操作。唯一的方法是使用元类,并__slots__在调用type.__new__.

此类元类的示例:

class MetaA(type):
    def __new__(mcls, name, bases, dct):
        slots = set(dct.get('__slots__', ()))
        slots.add('y')
        dct['__slots__'] = tuple(slots)
        return super().__new__(mcls, name, bases, dct)

class BaseA(metaclass=MetaA):
    pass

class A(BaseA):
    __slots__ = ('x',)

    def __init__(self):
        self.x = 1
        self.y = 2

print(A().x, A().y)
Run Code Online (Sandbox Code Playgroud)

如果没有元类,您可以施展一些魔法,复制已定义类中的所有内容并动态创建一个新类,但该代码有异味;)

def decorator(cls):
    slots = set(cls.__slots__)
    slots.add('y')
    dct = cls.__dict__.copy()
    for name in cls.__slots__:
        dct.pop(name)
    dct['__slots__'] = tuple(slots)
    return type(cls)(cls.__name__, cls.__bases__, dct)

@decorator
class A:
    __slots__ = ('x',)
    def __init__(self):
        self.x = self.y = 42

print(A().x, A().y)
Run Code Online (Sandbox Code Playgroud)

此类代码的主要缺点是,如果有人在您的装饰器之前应用另一个装饰器,并且,比方说,在某处创建对装饰类的引用,那么他们最终将存储对不同类的引用。元类也是如此——它们将执行两次。因此,元类方法更好,因为没有副作用。


__slots__为什么在创建类后不能真正更改的明确答案取决于您正在使用的 python 解释器的实现细节。例如,在 CPython 中,对于您定义的每个槽,类都有一个描述符(请参阅CPython 源代码中的PyMemberDescr_Type& PyMemberDefstruct),该描述符具有一个偏移参数,表示槽值在内部对象存储中的对齐位置。而且你根本没有在公共 Python API 中操作这些东西的工具。您可以用灵活性换取更少的内存使用(同样,在 CPython 中,就像在 PyPy 中一样,您会自动为所有类获得相同的内存效果)。

如果绝对需要修改__slots__,您可能可以编写一个 C 扩展(或使用ctypes)并执行此操作,但这并不是一个可靠的解决方案。