如何仅将关键字参数传递给__new__()而不传递给__init__()?

13 投票
2 回答
9750 浏览
提问于 2025-04-17 15:07

第一部分

我有一个设置,里面有一组我想要模拟的类。我的想法是,在我想要这样做的情况下,给构造函数传递一个 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)

我遇到的问题是,当 boolFalse 时,出现了以下错误:

TypeError: __init__() got an unexpected keyword argument 'mock'

如果我在 __init__ 中添加 mock 作为关键字参数,这样是可以工作的,但我想知道是否有办法避免这样做。我甚至从 kwargsdict 中弹出 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

我在一个单独的模块中定义了 RealObjectMockRealObject 类。现在我有两个问题:

  1. 如果 MockableMetaclassMockable 不在与 RealObject 类相同的模块中,当我提供 mock = True 时,eval 会引发 NameError
  2. 如果 mock = False,代码会进入一个无尽的递归,最终导致一个令人印象深刻的 RuntimeError: maximum recursion depth exceeded while calling a Python object。我猜这是因为 RealObject 的超类不再是 object,而是 Mockable

我该如何解决这些问题?我的方法不正确吗?我是否应该把 Mockable 作为一个装饰器?我试过这样做,但似乎没有效果,因为实例的 __new__ 似乎是只读的。

2 个回答

1

子类几乎是必不可少的,因为 __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 检查就会失败。

17

这件事要用元类来解决!:-)

在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):
    ...

撰写回答