Python:元类+包装方法+继承=问题

5

我在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

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


为什么你不能仅仅使用一个装饰器来做这件事呢? - Björn Pollex
你是否在寻找Python中的某种面向方面编程支持?请参考https://dev59.com/zXVC5IYBdhLWcg3wdw25,了解一些相关想法。这并不完全是你所要求的,而是试图解决你最初的需求。 - Makis
@Space_C0wb0y:因为我想在子类中重新声明test,而不必用5个装饰器重新装饰它!而且无论如何,那也解决不了我的问题:我从元类中包装方法的方式与使用装饰器完全相同。 - sebpiq
3个回答

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 块包裹起来,以确保在出现异常时重置上下文。

这是个好主意……但是如果由外部方法设置上下文,它就会出错。而且,所有其他的包装器在执行之前都需要检查上下文是否已经设置。 - sebpiq

0

好的,首先,你的“解决方案”真的很丑陋,但我想你已经知道了。 :-) 那么让我们来回答你的问题。

首先是一个隐含的“问题”:为什么不使用Python的上下文管理器?它们给你更漂亮的语法和几乎免费的错误管理。看看contextlib模块,它可以帮助你很多。特别是看看关于可重入性的部分

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

  • (更简洁)在进入上下文时,保持一个self.context列表,并在其中添加元素;在退出上下文时从中弹出元素。这样你总是可以得到你自己的上下文。

  • (更接近你想要的)只保持一个self.context,同时还有一个全局变量DEPTH。进入上下文时,DEPTH增加1;退出上下文时,DEPTH减少1;当DEPTH为0时,将self.context重置为None。

至于你的第二个问题,我必须说我不太理解你的意思。self本身就是正确的类型。如果A是B的子类,并且self是A的实例,那么self也是B的实例。如果无论你将self视为A的实例还是B的实例,self.some_minimum都是"错误"的,那意味着some_minimum并不是self的实例属性,而是A或B的类属性。对吗?因为A和B是不同的对象(它们的元类不同),所以它们在A和B上可以自由地不同。


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

包装代码被绕过了!!!这很hackish,但它可以工作...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接