Называете класс статическим методом внутри тела класса?


159

Когда я пытаюсь использовать статический метод из тела класса, и определяю статический метод, используя встроенную staticmethodфункцию в качестве декоратора, например:

class Klass(object):

    @staticmethod  # use as decorator
    def _stat_func():
        return 42

    _ANS = _stat_func()  # call the staticmethod

    def method(self):
        ret = Klass._stat_func() + Klass._ANS
        return ret

Я получаю следующую ошибку:

Traceback (most recent call last):<br>
  File "call_staticmethod.py", line 1, in <module>
    class Klass(object): 
  File "call_staticmethod.py", line 7, in Klass
    _ANS = _stat_func() 
  TypeError: 'staticmethod' object is not callable

Я понимаю, почему это происходит (привязка дескриптора) , и могу обойти это, вручную преобразовав _stat_func()метод static после его последнего использования, например, так:

class Klass(object):

    def _stat_func():
        return 42

    _ANS = _stat_func()  # use the non-staticmethod version

    _stat_func = staticmethod(_stat_func)  # convert function to a static method

    def method(self):
        ret = Klass._stat_func() + Klass._ANS
        return ret

Итак, мой вопрос:

Есть ли лучшие, как в более чистых или более «Pythonic», способы сделать это?


4
Если вы спрашиваете о Pythonicity, то стандартный совет не использовать staticmethodвообще. Они обычно более полезны как функции уровня модуля, и в этом случае ваша проблема не является проблемой. classmethodс другой стороны ...
Бенджамин Ходжсон

1
@poorsod: Да, я знаю об этой альтернативе. Однако в реальном коде, где я столкнулся с этой проблемой, делать функцию статическим методом, а не помещать ее на уровне модуля, имеет больше смысла, чем в простом примере, использованном в моем вопросе.
Мартино

Ответы:


180

staticmethodу объектов, по-видимому, есть __func__атрибут, хранящий исходную необработанную функцию (имеет смысл, что они должны были). Так что это будет работать:

class Klass(object):

    @staticmethod  # use as decorator
    def stat_func():
        return 42

    _ANS = stat_func.__func__()  # call the staticmethod

    def method(self):
        ret = Klass.stat_func()
        return ret

Помимо этого, хотя я подозревал, что у объекта staticmethod есть какой-то атрибут, хранящий исходную функцию, я понятия не имел о специфике. В духе обучения кого-то ловить рыбу, а не давать ему рыбу, это то, что я сделал, чтобы исследовать и выяснить это (C & P из моей сессии Python):

>>> class Foo(object):
...     @staticmethod
...     def foo():
...         return 3
...     global z
...     z = foo

>>> z
<staticmethod object at 0x0000000002E40558>
>>> Foo.foo
<function foo at 0x0000000002E3CBA8>
>>> dir(z)
['__class__', '__delattr__', '__doc__', '__format__', '__func__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> z.__func__
<function foo at 0x0000000002E3CBA8>

Подобные виды копания в интерактивном сеансе ( dirочень полезно) часто могут решить такие вопросы очень быстро.


Хорошее обновление ... Я как раз собирался спросить, откуда вы это знаете, так как я не вижу этого в документации - это меня немного нервирует, потому что это может быть "деталью реализации".
Мартино,

После дальнейшего прочтения я вижу, что __func__это просто другое название im_funcи было добавлено в Py 2.6 для прямой совместимости с Python 3.
Мартино

2
Понятно, поэтому технически это не документировано в этом контексте.
Мартино

1
@AkshayHazari __func__Атрибут статического метода возвращает вам ссылку на исходную функцию, точно так же, как если бы вы никогда не использовали staticmethodдекоратор. Поэтому, если вашей функции требуются аргументы, вам придется передавать их при вызове __func__. Сообщение об ошибке у вас звучит так, будто вы не дали ему никаких аргументов. Если бы stat_funcв примере этого поста было два аргумента, вы бы использовали_ANS = stat_func.__func__(arg1, arg2)
Бен

1
@AkshayHazari Я не уверен, что понимаю. Они могут быть переменными, они просто должны быть переменными в области видимости: либо определены ранее в той же области видимости, где определен класс (часто глобальный), либо определены ранее в рамках области видимости ( stat_funcсама такая переменная). Вы имели в виду, что не можете использовать атрибуты экземпляра определяемого вами класса? Это правда, но неудивительно; мы не имеем экземпляр класса, так как мы до сих пор определяющие класс! Но, во всяком случае, я просто имел в виду, что они заменяют любые аргументы, которые вы хотели бы передать; Вы могли бы использовать литералы там.
Бен

24

Это способ, которым я предпочитаю:

class Klass(object):

    @staticmethod
    def stat_func():
        return 42

    _ANS = stat_func.__func__()

    def method(self):
        return self.__class__.stat_func() + self.__class__._ANS

Я предпочитаю это решение Klass.stat_funcиз-за принципа СУХОЙ . Напоминает мне причину появления новогоsuper() в Python 3 :)

Но я согласен с другими, обычно лучшим выбором является определение функции уровня модуля.

Например, с @staticmethodфункцией рекурсия может выглядеть не очень хорошо (вам нужно нарушить принцип DRY, вызывая Klass.stat_funcвнутри Klass.stat_func). Это потому, что у вас нет ссылки на selfвнутренний статический метод. С функцией уровня модуля все будет выглядеть хорошо.


Хотя я согласен, что использование self.__class__.stat_func()в обычных методах имеет преимущества (СУХОЙ и все такое) по сравнению с использованием Klass.stat_func(), это не было темой моего вопроса - на самом деле я избегал использовать первое, чтобы не затуманить проблему несоответствия.
Мартино

1
На самом деле это не проблема СУХОЙ. self.__class__лучше, потому что если подкласс переопределяет stat_func, то Subclass.methodвызовет подкласс stat_func. Но, если честно, в такой ситуации гораздо лучше использовать реальный метод, а не статический.
asmeurer

@asmeurer: я не могу использовать реальный метод, потому что экземпляры класса еще не созданы - их не может быть, так как сам класс еще не был полностью определен.
Мартино

Мне нужен был способ вызова статического метода для моего класса (который наследуется; поэтому родительский объект действительно имеет функцию), и это, безусловно, лучше всего (и все равно будет работать, если я переопределю его позже).
whitey04

11

Как насчет введения атрибута класса после определения класса?

class Klass(object):

    @staticmethod  # use as decorator
    def stat_func():
        return 42

    def method(self):
        ret = Klass.stat_func()
        return ret

Klass._ANS = Klass.stat_func()  # inject the class attribute with static method value

1
Это похоже на мои первые попытки обойти, но я бы предпочел что-то, что было внутри класса ... частично, потому что задействованный атрибут имеет подчеркивание и является частным.
Мартино

11

Это происходит из-за того, что staticmethod является дескриптором и требует выборки атрибутов на уровне класса, чтобы использовать протокол дескриптора и получить истинный вызываемый объект.

Из исходного кода:

Он может быть вызван либо в классе (например C.f()), либо в экземпляре (например C().f()); экземпляр игнорируется за исключением его класса.

Но не прямо из класса, пока он определяется.

Но, как заметил один комментатор, на самом деле это не «Pythonic» дизайн. Просто используйте вместо этого функцию уровня модуля.


Как я уже сказал в своем вопросе, я понимаю, почему оригинальный код не работал. Можете ли вы объяснить (или предоставить ссылку на что-то, что делает), почему это считается непифоническим?
Мартино

Статический метод требует, чтобы сам объект класса работал правильно. Но если вы вызываете его из класса на уровне класса, класс на самом деле не полностью определен. Так что вы еще не можете сослаться на это. Можно вызывать static-метод извне класса после его определения. Но « Pythonic » на самом деле не очень хорошо определен, во многом как эстетика. Я могу сказать вам, что мое первое впечатление о том, что код не был благоприятным.
Кит

Впечатление от какой версии (или обеих)? А почему конкретно?
Мартино

Я могу только догадываться, какова ваша конечная цель, но мне кажется, что вам нужен динамический атрибут уровня класса. Это похоже на работу для метаклассов. Но опять же, возможно, существует более простой способ или другой способ взглянуть на дизайн, который устраняет и упрощает без ущерба для функциональности.
Кит

3
Нет, то, что я хочу сделать, на самом деле довольно просто - это выделить некоторый общий код и использовать его повторно, как для создания атрибута частного класса во время создания класса, так и позже в одном или нескольких методах класса. Этот общий код бесполезен вне класса, поэтому я, естественно, хочу, чтобы он был его частью. Использование метакласса (или декоратора класса) будет работать, но это кажется излишним для чего-то, что должно быть легко сделать, ИМХО.
Мартино

8

Как насчет этого решения? Он не зависит от знаний о @staticmethodреализации декоратора. Внутренний класс StaticMethod играет роль контейнера статических функций инициализации.

class Klass(object):

    class StaticMethod:
        @staticmethod  # use as decorator
        def _stat_func():
            return 42

    _ANS = StaticMethod._stat_func()  # call the staticmethod

    def method(self):
        ret = self.StaticMethod._stat_func() + Klass._ANS
        return ret

2
+1 за креативность, но я больше не беспокоюсь об использовании, __func__потому что это теперь официально задокументировано (see section Другие языковые изменения в Что нового в Python 2.7 и его ссылка на Выпуск 5982 ). Ваше решение еще более переносимо, поскольку, вероятно, оно также будет работать в версиях Python до версии 2.6 (когда __func__впервые было введено как синоним im_func).
Мартино

Это единственное решение, которое работает с Python 2.6.
Бенсельме

@benselme: Я не могу подтвердить ваше заявление, потому что у меня не установлен Python 2.6, но есть серьезные сомнения, что это единственный ...
martineau
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.