我正在尝试编写一种功能,该功能可以从类创建类而不修改原始类。
简单的解决方案(基于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实现的。如果尝试复制其他内置类型(如
int
或str
),则会遇到类似的问题。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