Почему декорирование класса нарушает протокол дескриптора, не позволяя объектам staticmethod вести себя должным образом?

Мне нужна небольшая помощь, чтобы понять тонкости протокола дескрипторов в 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функционировать так, как вы ожидаете, даже если класс упакован. По счастливой случайности, stdlib 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__сам по себе, чтобы это работало без суеты?

Спасибо.

9
задан robru 24 June 2012 в 08:53
поделиться