制作内置类的副本

时间:2018-05-04 13:15:46

标签: python python-3.x types copy cpython

我正在尝试编写从类创建类而不修改原始类的函数。

简单解决方案(基于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类(以及许多其他内置类)。

1 个答案:

答案 0 :(得分:1)

这里的问题是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如此难以复制有两个原因:

  1. 它是在C中实现的。如果您尝试复制其他内置类型,例如intstr,则会遇到类似的问题。
  2. type本身的实例的事实:

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

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

  3. 正如您所看到的,无法制作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