我正在尝试编写一种功能,该功能可以从类创建类而不修改原始类。

简单的解决方案(基于this answer

def class_operator(cls):
    namespace = dict(vars(cls))
    ...  # modifying namespace
    return type(cls.__qualname__, cls.__bases__, namespace)


除了type本身,它可以正常工作:

>>> class_operator(type)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: type __qualname__ must be a str, not getset_descriptor


在Python 3.2-Python 3.6上测试。

(我知道在当前版本中,对namespace对象中的可变属性的修改将更改原始类,但事实并非如此)

更新资料

即使我们从__qualname__中删除​​namespace参数

def class_operator(cls):
    namespace = dict(vars(cls))
    namespace.pop('__qualname__', None)
    return type(cls.__qualname__, cls.__bases__, namespace)


结果对象的行为不像原始的type

>>> type_copy = class_operator(type)
>>> type_copy is type
False
>>> type_copy('')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: descriptor '__init__' for 'type' objects doesn't apply to 'type' object
>>> type_copy('empty', (), {})
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: descriptor '__init__' for 'type' objects doesn't apply to 'type' object


为什么?

有人可以解释一下Python内部机制阻止复制type类(以及许多其他内置类)的机制。

最佳答案

这里的问题是type在其__qualname__中有一个__dict__,这是一个属性(即descriptor)而不是字符串:

>>> type.__qualname__
'type'
>>> vars(type)['__qualname__']
<attribute '__qualname__' of 'type' objects>


尝试将非字符串分配给类的__qualname__会引发异常:

>>> class C: pass
...
>>> C.__qualname__ = 'Foo'  # works
>>> C.__qualname__ = 3  # doesn't work
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only assign string to C.__qualname__, not 'int'


这就是为什么必须从__qualname__中删除​​__dict__的原因。

至于您的type_copy不可调用的原因:这是因为type.__call__拒绝不是type子类的任何内容。这对于3参数形式都是正确的:

>>> type.__call__(type, 'x', (), {})
<class '__main__.x'>
>>> type.__call__(type_copy, 'x', (), {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__init__' for 'type' objects doesn't apply to 'type' object


和单参数形式一样,它实际上仅将type作为其第一个参数使用:

>>> type.__call__(type, 3)
<class 'int'>
>>> type.__call__(type_copy, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type.__new__() takes exactly 3 arguments (1 given)


这不容易规避。修正3参数形式很简单:我们将副本设为type的空子类。

>>> type_copy = type('type_copy', (type,), {})
>>> type_copy('MyClass', (), {})
<class '__main__.MyClass'>


但是type的单参数形式非常麻烦,因为它仅在第一个参数为type时有效。我们可以实现自定义的__call__方法,但是该方法必须写在元类中,这意味着type(type_copy)type(type)不同。

>>> class TypeCopyMeta(type):
...     def __call__(self, *args):
...         if len(args) == 1:
...             return type(*args)
...         return super().__call__(*args)
...
>>> type_copy = TypeCopyMeta('type_copy', (type,), {})
>>> type_copy(3)  # works
<class 'int'>
>>> type_copy('MyClass', (), {})  # also works
<class '__main__.MyClass'>
>>> type(type), type(type_copy)  # but they're not identical
(<class 'type'>, <class '__main__.TypeCopyMeta'>)




type如此难以复制的原因有两个:


它是用C实现的。如果尝试复制其他内置类型(如intstr),则会遇到类似的问题。
type是其自身实例的事实:

>>> type(type)
<class 'type'>


这通常是不可能的。它模糊了类和实例之间的界限。这是实例和类属性的混乱积累。这就是为什么__qualname__type.__qualname__访问时为字符串,而以vars(type)['__qualname__']访问时为描述符的原因。




如您所见,不可能完美复制type。每个实现都有不同的权衡。

一种简单的解决方案是创建type的子类,该子类不支持单参数type(some_object)调用:

import builtins

def copy_class(cls):
    # if it's a builtin class, copy it by subclassing
    if getattr(builtins, cls.__name__, None) is cls:
        namespace = {}
        bases = (cls,)
    else:
        namespace = dict(vars(cls))
        bases = cls.__bases__

    cls_copy = type(cls.__name__, bases, namespace)
    cls_copy.__qualname__ = cls.__qualname__
    return cls_copy


精心设计的解决方案是制作一个自定义元类:

import builtins

def copy_class(cls):
    if cls is type:
        namespace = {}
        bases = (cls,)

        class metaclass(type):
            def __call__(self, *args):
                if len(args) == 1:
                    return type(*args)
                return super().__call__(*args)

        metaclass.__name__ = type.__name__
        metaclass.__qualname__ = type.__qualname__
    # if it's a builtin class, copy it by subclassing
    elif getattr(builtins, cls.__name__, None) is cls:
        namespace = {}
        bases = (cls,)
        metaclass = type
    else:
        namespace = dict(vars(cls))
        bases = cls.__bases__
        metaclass = type

    cls_copy = metaclass(cls.__name__, bases, namespace)
    cls_copy.__qualname__ = cls.__qualname__
    return cls_copy

07-24 09:45
查看更多