如何仅将关键字参数传递给__new__()而不传递给__init__()?
第一部分
我有一个设置,里面有一组我想要模拟的类。我的想法是,在我想要这样做的情况下,给构造函数传递一个 mock
的关键字参数,然后在 __new__
方法中拦截这个参数,返回一个模拟版本的对象。
它看起来像这样(根据 @mgilsons 的建议编辑了关键字查找):
class RealObject(object):
def __new__(cls, *args, **kwargs):
if kwargs.pop('mock', None):
return MockRealObject()
return super(RealObect, cls).__new__(cls, *args, **kwargs)
def __init__(self, whatever = None):
'''
Constructor
'''
#stuff happens
然后我这样调用构造函数:
ro = RealObject(mock = bool)
我遇到的问题是,当 bool
为 False
时,出现了以下错误:
TypeError: __init__() got an unexpected keyword argument 'mock'
如果我在 __init__
中添加 mock
作为关键字参数,这样是可以工作的,但我想知道是否有办法避免这样做。我甚至从 kwargs
的 dict
中弹出 mock
。
这也是一个关于设计的问题。有没有更好的方法来做到这一点?(当然有!)我想尝试这种方式,而不使用工厂、超类或其他任何东西。但我还是应该使用其他关键字吗?比如 __call__
?
第二部分,基于 jsbueno 的回答
所以我想把元类和 __new__
函数提取到一个单独的模块中。我这样做了:
class Mockable(object):
def __new__(cls, *args, **kwargs):
if kwargs.pop('mock', None):
mock_cls = eval('{0}{1}'.format('Mock',cls.__name__))
return super(mock_cls, mock_cls).__new__(mock_cls)
return super(cls, cls).__new__(cls,*args, **kwargs)
class MockableMetaclass(type):
def __call__(self, *args, **kwargs):
obj = self.__new__(self, *args, **kwargs)
if "mock" in kwargs:
del kwargs["mock"]
obj.__init__(*args, **kwargs)
return obj
我在一个单独的模块中定义了 RealObject
和 MockRealObject
类。现在我有两个问题:
- 如果
MockableMetaclass
和Mockable
不在与RealObject
类相同的模块中,当我提供mock = True
时,eval
会引发NameError
。 - 如果
mock = False
,代码会进入一个无尽的递归,最终导致一个令人印象深刻的RuntimeError: maximum recursion depth exceeded while calling a Python object
。我猜这是因为RealObject
的超类不再是object
,而是Mockable
。
我该如何解决这些问题?我的方法不正确吗?我是否应该把 Mockable
作为一个装饰器?我试过这样做,但似乎没有效果,因为实例的 __new__
似乎是只读的。
2 个回答
子类几乎是必不可少的,因为 __new__
方法总是把参数传递给构造函数,也就是 __init__
方法。如果你通过类装饰器添加一个子类作为混入类(mixin),那么你就可以在子类的 __init__
方法中拦截 mock
参数:
def mock_with(mock_cls):
class MockMixin(object):
def __new__(cls, *args, **kwargs):
if kwargs.pop('mock'):
return mock_cls()
return super(MockMixin, cls).__new__(cls, *args, **kwargs)
def __init__(self, *args, **kwargs):
kwargs.pop('mock')
super(MockMixin, self).__init__(*args, **kwargs)
def decorator(real_cls):
return type(real_cls.__name__, (MockMixin, real_cls), {})
return decorator
class MockRealObject(object):
pass
@mock_with(MockRealObject)
class RealObject(object):
def __init__(self, whatever=None):
pass
r = RealObject(mock=False)
assert isinstance(r, RealObject)
m = RealObject(mock=True)
assert isinstance(m, MockRealObject)
另一种方法是让子类的 __new__
方法返回 RealObject(cls, *args, **kwargs)
;在这种情况下,返回的对象就不是子类的实例。不过这样的话,isinstance
检查就会失败。
这件事要用元类来解决!:-)
在Python中,当你创建一个新的对象时,负责调用__new__
和__init__
这两个方法的代码是在类的元类的__call__
方法里(或者说是跟这个意思相同的地方)。
换句话说,当你执行:
RealObject()
时,实际上调用的是RealObject.__class__.__call__
这个方法。因为如果没有明确指定元类,默认的元类是type
,所以实际上调用的是type.__call__
。
大多数关于元类的教程都是在讲如何重写__new__
方法,这样可以在类被创建时自动执行一些操作。但如果我们重写__call__
,就可以在类被实例化时执行操作。
在这种情况下,只需要在调用__init__
之前,去掉任何“mock”这个关键字参数(如果有的话):
class MetaMock(type):
def __call__(cls, *args, **kw):
obj = cls.__new__(cls, *args, **kw)
if "mock" in kw:
del kw["mock"]
obj.__init__(*args, **kw)
return obj
class RealObject(metaclass=MetaMock):
...