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