9

我需要一点帮助来理解 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 标准库包含了一个叫做 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将静态方法复制到包装函数,但它不可调用?为什么不?我在这里错过了什么?

我正在玩这个,实际上我想出了我自己的重新实现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!

您将采取什么方法使静态方法在装饰类中按预期运行?这是最好的方法吗?为什么内置的静态方法不__call__自己实现,以便它可以正常工作而不大惊小怪?

谢谢。

4

2 回答 2

4

问题是您正在将类型Stub从类更改为函数。这是一个非常严重的违规行为,事情发生了也就不足为奇了。

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

wrt 实际上在做你感兴趣的事情(某种单例?),你可能希望你的装饰器返回一个__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(在包装类中变得封闭)和.clsWrapper.__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__,因此您必须解决这个问题(例如,通过包装__init__到现有实例上的短路)。

作为附录:在您知道的情况下,您可能需要付出很多努力才能使您的 function-wrapping-a-class 装饰器工作,但您仍然会遇到问题 - 例如,isinstance(myObject, Stub)没有机会作为Stub不再是一个type

于 2012-06-25T00:32:03.450 回答
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__.

于 2012-06-24T20:42:04.453 回答