Está chamando classe staticmethod dentro do corpo da classe?

159

Quando tento usar um método estático de dentro do corpo da classe e defino o método estático usando a staticmethodfunção interna como decorador, desta forma:

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

Estou tendo o erro a seguir:

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

Entendo por que isso está acontecendo (ligação do descritor) e posso contornar isso convertendo-o manualmente _stat_func()em um método estático após seu último uso, da seguinte forma:

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

Então, minha pergunta é:

Existem melhores, como em formas mais limpas ou mais "pitônicas", para conseguir isso?

Martineau
fonte
4
Se você está perguntando sobre pitonicidade, o conselho padrão é não usar staticmethod. Eles geralmente são mais úteis como funções no nível do módulo; nesse caso, seu problema não é um problema. classmethod, Por outro lado ...
Benjamin Hodgson
1
@poorsod: Sim, estou ciente dessa alternativa. No entanto, no código real em que encontrei esse problema, tornar a função um método estático em vez de colocá-lo no nível do módulo faz mais sentido do que no exemplo simples usado na minha pergunta.
martineau

Respostas:

180

staticmethodos objetos aparentemente têm um __func__atributo que armazena a função bruta original (faz sentido que eles precisassem). Então, isso vai funcionar:

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

Além disso, embora eu suspeitasse que um objeto staticmethod tivesse algum tipo de atributo armazenando a função original, eu não tinha ideia dos detalhes. No espírito de ensinar alguém a pescar em vez de dar a ele um peixe, foi o que eu fiz para investigar e descobrir isso (um C&P da minha sessão em 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>

Tipos semelhantes de escavação em uma sessão interativa ( diré muito útil) geralmente podem resolver esses tipos de perguntas muito rapidamente.

Ben
fonte
Boa atualização ... Eu estava prestes a perguntar como você sabia disso, uma vez que não a vejo na documentação - o que me deixa um pouco nervoso em usá-la porque pode ser um "detalhe de implementação".
martineau
Depois de ler mais, vejo que esse __func__é apenas outro nome im_funce foi adicionado ao Py 2.6 para compatibilidade com versões anteriores do Python 3.
martineau
2
Entendo, tecnicamente, isso não está documentado neste contexto.
martineau
1
@AkshayHazari O __func__atributo de um método estático fornece uma referência à função original, exatamente como se você nunca tivesse usado o staticmethoddecorador. Portanto, se sua função exigir argumentos, você precisará passá-los ao chamar __func__. A mensagem de erro de que você parece parece que não deu argumentos. Se o stat_funcexemplo deste post tivesse dois argumentos, você usaria_ANS = stat_func.__func__(arg1, arg2)
Ben
1
@AkshayHazari Não sei se entendi. Eles podem ser variáveis, apenas precisam ser variáveis ​​dentro do escopo: definidas anteriormente no mesmo escopo em que a classe é definida (geralmente global) ou definidas anteriormente no escopo da classe ( stat_funcela mesma é uma variável). Você quis dizer que não pode usar atributos de instância da classe que está definindo? Isso é verdade, mas não surpreende; não temos uma instância da classe, pois ainda estamos definindo a classe! Mas, de qualquer maneira, eu apenas as pretendia como suporte para quaisquer argumentos que você desejasse passar; você poderia usar literais lá.
Ben
24

É assim que eu prefiro:

class Klass(object):

    @staticmethod
    def stat_func():
        return 42

    _ANS = stat_func.__func__()

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

Prefiro esta solução a Klass.stat_func, por causa do princípio DRY . Lembra-me da razão pela qual há um novosuper() no Python 3 :)

Mas eu concordo com os outros, geralmente a melhor opção é definir uma função no nível do módulo.

Por exemplo, com a @staticmethodfunção, a recursão pode não parecer muito boa (você precisaria quebrar o princípio DRY ligando para Klass.stat_funcdentro Klass.stat_func). Isso ocorre porque você não tem referência ao selfmétodo estático interno. Com a função no nível do módulo, tudo ficará bem.

Jan Vorcak
fonte
Embora eu concorde que o uso de self.__class__.stat_func()métodos regulares tenha vantagens (DRY e tudo isso) sobre o uso Klass.stat_func(), esse não foi realmente o tópico da minha pergunta - na verdade, evitei usá-lo para não obscurecer a questão da inconsistência.
martineau
1
Não é realmente uma questão de DRY em si. self.__class__é melhor porque se uma subclasse substituir stat_func, Subclass.methodela chamará a subclasse stat_func. Mas, para ser completamente honesto, nessa situação, é muito melhor usar apenas um método real em vez de estático.
asmeurer
@asmeurer: Eu não posso usar um método real porque ainda não foram criadas instâncias da classe - elas não podem ser, uma vez que a própria classe ainda nem foi completamente definida ainda.
28519 martineau
Eu queria uma maneira de chamar o método estático para minha classe (que é herdada; portanto, o pai realmente tem a função) e isso parece melhor de longe (e ainda funcionará se eu a substituir posteriormente).
whitey04
11

Que tal injetar o atributo de classe após a definição da classe?

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
Pedro Romano
fonte
1
Isso é semelhante às minhas primeiras tentativas de contornar o problema, mas eu preferiria algo que estivesse dentro da classe ... parcialmente porque o atributo envolvido tem um sublinhado de destaque e é privado.
martineau
11

Isso ocorre porque o staticmethod é um descritor e requer uma busca de atributo no nível da classe para exercitar o protocolo do descritor e obter a verdadeira capacidade de chamada.

Do código fonte:

Pode ser chamado na classe (por exemplo C.f()) ou em uma instância (por exemplo C().f()); a instância é ignorada, exceto por sua classe.

Mas não diretamente de dentro da classe enquanto ela está sendo definida.

Mas como um comentarista mencionou, esse não é realmente um design "pitonico". Basta usar uma função de nível de módulo.

Keith
fonte
Como disse na minha pergunta, entendo por que o código original não funcionou. Você pode explicar (ou fornecer um link para algo que faz) por que é considerado não-Pitônico?
martineau
O staticmethod requer que o próprio objeto de classe funcione corretamente. Mas se você chamá-lo de dentro da classe no nível da classe, a classe não está realmente totalmente definida nesse ponto. Então você não pode fazer referência a isso ainda. Não há problema em chamar o método static de fora da classe, depois de definido. Mas " Pythonic " não é realmente bem definido, assim como a estética. Posso lhe dizer que minha primeira impressão desse código não foi favorável.
Keith
Impressão de qual versão (ou de ambas)? E por que, especificamente?
martineau
Só posso adivinhar qual é seu objetivo final, mas parece-me que você deseja um atributo dinâmico em nível de classe. Parece um trabalho para metaclasses. Porém, mais uma vez, pode haver uma maneira mais simples ou outra para observar o design que elimina e simplifica sem sacrificar a funcionalidade.
Keith
3
Não, o que eu quero fazer é realmente bastante simples - que é fatorar um código comum e reutilizá-lo, tanto para criar um atributo de classe privada no momento da criação da classe, quanto posteriormente dentro de um ou mais métodos de classe. Esse código comum não tem uso fora da classe, então, naturalmente, quero que faça parte dele. Usar uma metaclasse (ou um decorador de classe) funcionaria, mas isso parece exagero para algo que deveria ser fácil de fazer, IMHO.
martineau
8

E essa solução? Não depende do conhecimento da @staticmethodimplementação do decorador. Classe interna StaticMethod é reproduzida como um contêiner de funções de inicialização estática.

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
Schatten
fonte
2
+1 para criatividade, mas não estou mais preocupado em usá- __func__lo, porque agora está oficialmente documentado (consulte a seção Outras alterações de idioma do que há de novo no Python 2.7 e sua referência ao problema 5982 ). Sua solução é ainda mais portátil, pois provavelmente também funcionaria nas versões Python anteriores ao 2.6 (quando __func__foi introduzido pela primeira vez como sinônimo de im_func).
Martineau 16/05
Esta é a única solução que funciona com o Python 2.6.
benselme
@benselme: Eu não pode verificar o seu pedido, porque eu não tenho Python 2.6 instalado, mas a dúvida séria é a única único ...
martineau