Python:metaclass + 装饰方法 + 继承 = 问题

5 投票
3 回答
1010 浏览
提问于 2025-04-16 13:09

我在使用Python时遇到了一个问题,找不到简单的解决办法...

在调用某些方法时,我希望在执行方法之前和之后运行一些代码。这样做的目的是为了自动设置和清理一个叫做context的变量。

为了实现这个目标,我声明了一个元类:

class MyType(type):
    def __new__(cls, name, bases, attrs):
        #wraps the 'test' method to automate context management and other stuff
        attrs['test'] = cls.other_wrapper(attrs['test'])
        attrs['test'] = cls.context_wrapper(attrs['test'])
        return super(MyType, cls).__new__(cls, name, bases, attrs)

    @classmethod
    def context_wrapper(cls, operation):
        def _manage_context(self, *args, **kwargs):
            #Sets the context to 'blabla' before the execution
            self.context = 'blabla'
            returned = operation(self, *args, **kwargs)
            #Cleans the context after execution
            self.context = None
            return returned
        return _manage_context

    @classmethod
    def other_wrapper(cls, operation):
        def _wrapped(self, *args, **kwargs):
            #DO something with self and *args and **kwargs
            return operation(self, *args, **kwargs)
        return _wrapped

这个方法效果很好:

class Parent(object):

    __metaclass__ = MyType

    def test(self):
        #Here the context is set:
        print self.context #prints blabla

但是一旦我想要创建一个Parent的子类,就会出现问题,当我用super调用父类的方法时:

class Child(Parent):
    def test(self):
        #Here the context is set too
        print self.context #prints blabla
        super(Child, self).test()
        #But now the context is unset, because Parent.test is also wrapped by _manage_context
        #so this prints 'None', which is not what was expected
        print self.context

我考虑过在设置新值之前先保存上下文,但这只能部分解决问题...

确实,(等一下,这个有点难解释),父类的方法被调用了,包装器也执行了,但它们接收到的*args**kwargs是针对Parent.test的,而self是一个Child实例,所以如果我想用*args**kwargs来验证self的属性,它们的值就不相关了(比如为了自动验证),举个例子:

@classmethod
def validation_wrapper(cls, operation):
    def _wrapped(self, *args, **kwargs):
        #Validate the value of a kwarg
        #But if this is executed because we called super(Child, self).test(...
        #`self.some_minimum` will be `Child.some_minimum`, which is irrelevant
        #considering that we called `Parent.test`
        if not kwarg['some_arg'] > self.some_minimum:
            raise ValueError('Validation failed')
        return operation(self, *args, **kwargs)
    return _wrapped

所以基本上,要解决这个问题,我看到两个解决方案:

  1. 在用super(Child, self)调用方法时,防止包装器被执行

  2. 确保self总是“正确”的类型

这两种解决方案对我来说似乎都不可能... 有人有办法解决这个问题吗?有什么建议吗?

3 个回答

0

好的,首先,你的“解决方案”看起来真的不太好,但我想你自己也知道这一点。:-) 那么我们来回答你的问题吧。

首先,有个隐含的“问题”:你为什么不使用Python的上下文管理器呢?它们能让你的代码看起来更简洁,而且错误处理也变得简单得多。你可以看看contextlib模块,它会对你大有帮助。特别是看看关于可重入的部分

然后你会发现,很多人在尝试堆叠上下文管理器时会遇到问题。这并不奇怪,因为要正确支持递归,你需要一个值的堆栈,而不是单个值。[你可以查看一些可重入上下文管理器的源代码,比如redirect_stdout,看看它是怎么处理的。] 所以你的context_wrapper应该:

  • (更简洁)保持一个self.context的列表,进入上下文时添加到列表中,退出时从列表中移除。这样你总是能得到你自己的上下文。

  • (更像你想要的那样)保持一个单独的self.context,同时还有一个全局值DEPTH,进入时加一,退出时减一,当DEPTH为0时,self.context重置为None。

至于你的第二个问题,我必须说我有点不太理解你。self 正确的类型。如果A是B的子类,而self是A的实例,那么self也是B的实例。如果self.some_minimum在你认为self是A的实例还是B的实例时都是“错误的”,那就意味着some_minimum并不是真正属于self的实例属性,而是A或B的类属性,对吧?它们在A和B上可以自由不同,因为A和B是不同的对象(属于它们的元类)。

1

那么,你能不能直接检查一下在 _manage_context 里上下文是否已经设置好了呢?就像这样:

def _manage_context(self, *args, **kwargs):
    #Sets the context to 'blabla' before the execution
    if self.context is None:
        self.context = 'blabla'
        returned = operation(self, *args, **kwargs)
        #Cleans the context after execution
        self.context = None
        return returned
    else:
        return operation(self, *args, **kwargs)

另外,这个最好放在一个 try-catch 块里,这样如果出现错误的话,可以确保上下文能够被重置。

0

其实我发现了一种方法,可以防止在用 super(Child, self) 调用方法时执行包装器:

class MyType(type):
    def __new__(cls, name, bases, attrs):
        #wraps the 'test' method to automate context management and other stuff
        new_class = super(MyType, cls).__new__(cls, name, bases, attrs)
        new_class.test = cls.other_wrapper(new_class.test, new_class)

    @classmethod
    def other_wrapper(cls, operation, new_class):
        def _wrapped(self, *args, **kwargs):
            #DO something with self and *args and **kwargs ...
            #ONLY if self is of type *new_class* !!!
            if type(self) == new_class:
                pass #do things
            return operation(self, *args, **kwargs)
        return _wrapped

这样,当调用时:

super(Child, self).a_wrapped_method

包装的代码就被绕过了!!!这有点像黑科技,但确实有效……

撰写回答