functools.wraps 返回 dict 类型的 __dict__ 而不是 mappingproxy

Posted

技术标签:

【中文标题】functools.wraps 返回 dict 类型的 __dict__ 而不是 mappingproxy【英文标题】:functools.wraps returning a __dict__ of type dict instead of type mappingproxy 【发布时间】:2017-07-04 20:54:29 【问题描述】:

这是我在这里的第一个问题,经过长时间的咨询。我在谷歌上搜索了很多关于这个问题的信息,但我认为我没有找到任何有用的信息来解决它。

我正在尝试在 Python 3 中为我的代码创建一个通用的弃用 python 装饰器。目标是正确包装一个函数或类做两件事:打印关于弃用的警告,并在 __doc__ 前面加上弃用信息( sphinx 也可以处理它。我想我已经整理出了大部分我想要的东西,我还在为 Sphinx 苦苦挣扎,但我发现了一些对 functools.wraps 的行为感到好奇的东西。基本上,包装类的__dict__ 的类型为dict,而未包装的类的类型为mappingproxy

这是一些展示代码:

import functools

class Deprecated(object):
    def __call__(self, cls):
        myassigns = functools.WRAPPER_ASSIGNMENTS
        myassigns += ('__mro__',)

        @functools.wraps(cls, assigned=myassigns)
        def new_func(*args, **kwargs):
            warnings.warn(message, category=DeprecationWarning, stacklevel=2)
            return cls(*args, **kwargs)

        return new_func


@Deprecated()
class SomeOldClass:
    def some_method(self, x, y):
        return x + y

class SomeClass:
    def some_method(self, x, y):
        return x + y

print(type(SomeOldClass.__dict__))
print(type(SomeClass.__dict__))

还有输出:

<class 'dict'>
<class 'mappingproxy'>

functools.wraps 是行为不端还是我做错了什么?

【问题讨论】:

你的“包装类”是一个函数。你期望functools.wraps 做什么? 您是要包装some_method 吗?因为按原样,您包装了类本身,这种工作方式(没有深入研究,但我相信它应该在您创建实例时发出警告),但这也意味着名称指的是包装函数,而不是类不再是,所以在SomeOldClass 上看不到与类相关的东西,而无需显式钻取到__wrapped__ 属性。如果你想以这种方式弃用类,你的包装器应该深入到类中并显式包装__init____new__(返回原始cls),而不是返回包装类的函数。 @user2357112 我希望它能够掩盖我的包装功能,所以看起来它是原始类,除了我的更改。具体来说,我希望像 help() 这样的函数能够在我已弃用的​​类上正常工作。 @ShadowRanger 感谢您的反馈!我可以尝试包装__init__,没想到。这种方法的问题是我不能轻易地添加__doc__,可以吗? @j3mdamas:如果你包装类本身,你可以修改它的文档,然后显式包装它的__init__ 并重新分配它。你仍然返回原来的cls,你只是在改变它的属性。 【参考方案1】:

functools.wrap 不是行为不端,技术上你也没有做错什么,你已经返回了 当调用 将返回实例的函数。

包装后,SomeOldClass 是你包装并返回的函数new_func

>>> SomeOldClass
<function __main__.SomeOldClass>

不是班级。未装饰的SomeClass 被保留为__dict__mappingproxy 的类。

【讨论】:

感谢您的回答!虽然很难找到装饰类的工作示例,所以我采用了这种方法。【参考方案2】:

我强烈建议让你的包装类知道,所以它不会包装类本身:

class Deprecated(object):
    def __init__(self, messagefmt='wrapped is deprecated'):
        self.messagefmt = messagefmt

    def __call__(self, wrapped):
        message = self.messagefmt.format(wrapped=wrapped.__name__)
        wrappingclass = isinstance(wrapped, type)
        if wrappingclass:
            wrappedfunc = wrapped.__init__
        else:
            wrappedfunc = wrapped

        @functools.wraps(wrappedfunc)
        def new_func(*args, **kwargs):
            warnings.warn(message, category=DeprecationWarning, stacklevel=2)
            return wrappedfunc(*args, **kwargs)
        wrapped.__doc__ = 'Deprecated\n\n' + message + '\n\n' + (wrapped.__doc__ or '')

        if wrappingclass:
            wrapped.__init__ = new_func
            return wrapped
        else:
            return new_func

这让你可以在两个类中使用它:

 @Deprecated()
 class SomeOldClass:
     def some_method(self, x, y):
         return x + y

不掩盖类行为和功能(可能带有自定义弃用消息):

 @Deprecated('foo is dead')
 def foo():
     pass

所以当你使用它们时:

 >>> import warnings
 >>> warnings.filterwarnings('always', category=DeprecationWarning)
 >>> SomeOldClass()
 /usr/local/bin/ipython3:1: DeprecationWarning: SomeOldClass is deprecated
   #!/usr/bin/python3
 <__main__.SomeOldClass at 0x7fea8f744a20>
 >>> foo()
 /usr/local/bin/ipython3:1: DeprecationWarning: foo is dead
   #!/usr/bin/python3

同样地,两者的__doc__ 都被修改以添加弃用通知(您可以将其调整为 Sphinxify,我只是提供基本示例)。

【讨论】:

非常感谢!这就是我一直在寻找的答案!我将对其进行测试,但我确信它有效。 @j3mdamas:我在显示的案例上对其进行了测试(并确保它可以正确修改 __doc__ 字符串,class 仍然是一个类等),但我确定有一些边缘情况(例如,我对类与非类的测试在 Py2 上对旧式类不友好,因此您需要对其进行一些调整以支持旧式类)。无论如何,它应该是相当接近的,只需最少的调整。 只是一个小评论:对于方法,__doc__ 没有更新。我把作业移到@functools.wraps(wrappedfunc) 之前,它开始工作了。 最后的评论:非常感谢您的示例。完美运行,我希望任何试图装饰班级的人都能找到这个答案。我不需要与 Py2 兼容,所以我很好。关于狮身人面像,我的问题是由于包装器将类变成了一个函数。既然你的代码解决了这个问题,我在 Sphinx 上的所有问题都得到了解决:) 非常感谢!

以上是关于functools.wraps 返回 dict 类型的 __dict__ 而不是 mappingproxy的主要内容,如果未能解决你的问题,请参考以下文章

Python functools.wraps 等价于类

[转] functools.wraps定义函数装饰器

python functools.wraps 实例

@functools.wraps(func)

python functools.wraps装饰器模块

python functools.wraps