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

时间:2012-06-24 19:53:58

标签: python python-2.7 decorator

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

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

此时,这表现得如预期,但这里发生的事情有点微妙:当你调用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!

到目前为止一切顺利。接下来,我需要将类包装在一个将用于自定义类实例化的装饰器中 - 装饰器将确定是允许新的实例化还是提供缓存的实例:

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!"

现在,自然这部分原样会被打破静态方法,因为这个类现在隐藏在它的装饰器后面,即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!

到目前为止,我了解这里发生了什么。我的目标是恢复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

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

>>> 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!

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

我正在讨论这个问题,我实际上想出了我自己的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!"

确实它的工作方式完全符合预期:

>>> 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!

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

感谢。

2 个答案:

答案 0 :(得分:4)

问题在于您正在将Stub的类型从类更改为函数。这是一个非常严重的违规行为,事情正在破裂并不奇怪。

staticmethod破坏的技术原因是functools.wraps通过复制__name____doc____module__等方式起作用。(来源:{{ 3}})从包装实例到包装器,同时从包装实例的__dict__更新包装器__dict__。现在应该很清楚staticmethod为什么不起作用 - 它的描述符协议是在一个函数而不是一个类上调用的,所以它放弃了返回一个绑定的可调用对象,只返回它的不可调用的自我。

w.r.t。实际上做你感兴趣的东西(某种Singleton?),你可能希望你的装饰者返回一个具有所需行为的__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

请注意装饰器的wrapped_cls参数(在包装器类中已关闭)与cls的{​​{1}}参数之间的区别。

请注意,在包装类的类上使用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 ,因此您必须解决此问题(例如,通过将__init__包裹到短路中在现有实例上。)

作为附录:有可能让你的函数包装一个类装饰器在你知道的情况下付出很多努力,但你仍然会遇到问题 - 例如,{{1由于__init__不再是isinstance(myObject, Stub)

,因此无法工作

答案 1 :(得分:2)

你几乎做了我本来会做的事情:

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

应该这样做。 问题可能是__init__返回的旧实例也会调用__new__