为什么装饰类会破坏描述符协议,从而阻止staticmethod对象按预期行为?

rob*_*bru 9 python decorator python-2.7

我需要一些帮助来理解Python中描述符协议的细微之处,因为它特别涉及staticmethod对象的行为.我将从一个简单的例子开始,然后迭代地扩展它,检查它在每一步的行为:

class Stub:
    @staticmethod
    def do_things():
        """Call this like Stub.do_things(), with no arguments or instance."""
        print "Doing things!"
Run Code Online (Sandbox Code Playgroud)

在这一点上,这表现得如预期的那样,但这里发生的事情有点微妙:当你打电话时Stub.do_things(),你不是直接调用do_things.相反,它Stub.do_things指的是一个staticmethod实例,它将我们想要的函数包装在它自己的描述符协议中,这样你实际上就是调用它staticmethod.__get__,它首先返回我们想要的函数,然后被调用.

>>> Stub
<class __main__.Stub at 0x...>
>>> Stub.do_things
<function do_things at 0x...>
>>> Stub.__dict__['do_things']
<staticmethod object at 0x...>
>>> Stub.do_things()
Doing things!
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.接下来,我需要将类包装在将用于自定义类实例化的装饰器中 - 装饰器将确定是允许新的实例化还是提供缓存的实例:

def deco(cls):
    def factory(*args, **kwargs):
        # pretend there is some logic here determining
        # whether to make a new instance or not
        return cls(*args, **kwargs)
    return factory

@deco
class Stub:
    @staticmethod
    def do_things():
        """Call this like Stub.do_things(), with no arguments or instance."""
        print "Doing things!"
Run Code Online (Sandbox Code Playgroud)

现在,这个部分当然会被打破静态方法,因为类现在隐藏在它的装饰器后面,即Stub根本不是一个类,但是它的一个实例factory能够Stub在你调用它时产生实例.确实:

>>> Stub
<function factory at 0x...>
>>> Stub.do_things
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'do_things'
>>> Stub()
<__main__.Stub instance at 0x...>
>>> Stub().do_things
<function do_things at 0x...>
>>> Stub().do_things()
Doing things!
Run Code Online (Sandbox Code Playgroud)

到目前为止,我了解这里发生了什么.我的目标是恢复staticmethods按照你期望的方式运行的能力,即使这个类被包装了.幸运的是,Python stdlib包含一些名为functools的东西,它提供了一些仅用于此目的的工具,即使函数的行为更像它们包装的其他函数.所以我把装饰师改成这样:

def deco(cls):
    @functools.wraps(cls)
    def factory(*args, **kwargs):
        # pretend there is some logic here determining
        # whether to make a new instance or not
        return cls(*args, **kwargs)
    return factory
Run Code Online (Sandbox Code Playgroud)

现在,事情开始变得有趣:

>>> Stub
<function Stub at 0x...>
>>> Stub.do_things
<staticmethod object at 0x...>
>>> Stub.do_things()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'staticmethod' object is not callable
>>> Stub()
<__main__.Stub instance at 0x...>
>>> Stub().do_things
<function do_things at 0x...>
>>> Stub().do_things()
Doing things!
Run Code Online (Sandbox Code Playgroud)

等等.... 什么? functools将staticmethod复制到包装函数,但它不可调用?为什么不?我在这里想念的是什么?

我正在讨论这个问题,我实际上想出了它自己的重新实现,staticmethod它允许它在这种情况下运行,但我真的不明白为什么它是必要的,或者这是否是这个问题的最佳解决方案.这是完整的例子:

class staticmethod(object):
    """Make @staticmethods play nice with decorated classes."""

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        """Provide the expected behavior inside decorated classes."""
        return self.func(*args, **kwargs)

    def __get__(self, obj, objtype=None):
        """Re-implement the standard behavior for undecorated classes."""
        return self.func

def deco(cls):
    @functools.wraps(cls)
    def factory(*args, **kwargs):
        # pretend there is some logic here determining
        # whether to make a new instance or not
        return cls(*args, **kwargs)
    return factory

@deco
class Stub:
    @staticmethod
    def do_things():
        """Call this like Stub.do_things(), with no arguments or instance."""
        print "Doing things!"
Run Code Online (Sandbox Code Playgroud)

确实,它的工作方式完全如预期

>>> Stub
<function Stub at 0x...>
>>> Stub.do_things
<__main__.staticmethod object at 0x...>
>>> Stub.do_things()
Doing things!
>>> Stub()
<__main__.Stub instance at 0x...>
>>> Stub().do_things
<function do_things at 0x...>
>>> Stub().do_things()
Doing things!
Run Code Online (Sandbox Code Playgroud)

你会采取什么方法使静态方法在装饰类中表现如预期?这是最好的方法吗?为什么内置静态方法不能__call__自行实现,以便它可以毫不费力地工作?

谢谢.

eca*_*mur 4

问题是您正在将类型Stub从类更改为函数。这是一个相当严重的违规行为,出现问题也就不足为奇了。

您的 s 被破坏的技术原因staticmethodfunctools.wraps通过将__name__,__doc____module__(来源:http://hg.python.org/cpython/file/3.2/Lib/functools.py)从包装实例复制到包装器来工作,同时__dict__从包装实例的__dict__. 现在应该很明显为什么staticmethod不起作用了 - 它的描述符协议是在函数而不是类上调用的,因此它放弃返回绑定的可调用对象,而只返回其不可调用的自身。

当实际做你感兴趣的事情(某种单例?)时,你可能希望你的装饰器返回一个__new__具有所需行为的类。您无需担心__init__被称为不需要的,只要您的包装类__new__实际上不返回包装类类型的值,而是返回包装类的实例:

def deco(wrapped_cls):
    @functools.wraps(wrapped_cls)
    class Wrapper(wrapped_cls):
        def __new__(cls, *args, **kwargs):
            ...
            return wrapped_cls(*args, **kwargs)
    return Wrapper
Run Code Online (Sandbox Code Playgroud)

wrapped_cls请注意装饰器的参数(在包装类中被封闭)和 的cls参数之间的区别Wrapper.__new__

functools.wraps请注意,在包装类的类上使用是完全可以的- 只是不能在包装函数的类上使用!

您还可以修改包装的类,在这种情况下您不需要functools.wraps

def deco(wrapped_cls):
    def __new__(cls, *args, **kwargs)
        ...
        return super(wrapped_cls, cls)(*args, **kwargs)
    wrapped_cls.__new__ = classmethod(__new__)
    return wrapped_cls
Run Code Online (Sandbox Code Playgroud)

但请注意,此方法最终__init__调用现有实例,因此您必须解决该问题(例如,通过__init__在现有实例上进行短路)。

作为附录:也许可以让你的函数包装类装饰器在你知道的情况下工作,但你仍然会遇到问题 - 例如,isinstance(myObject, Stub)没有机会像Stub不再是了type