¿Llamando al método estático de clase dentro del cuerpo de la clase?

159

Cuando intento usar un método estático desde dentro del cuerpo de la clase, y definir el método estático usando la staticmethodfunción incorporada como decorador, así:

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

Obtuve el siguiente error:

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

Entiendo por qué sucede esto (enlace del descriptor) , y puedo _stat_func()solucionarlo convirtiéndolo manualmente en un método estático después de su último uso, así:

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

Entonces mi pregunta es:

¿Hay mejores formas, como en limpia o más "pitón", de lograr esto?

Martineau
fuente
44
Si está preguntando acerca de Pythonicity, entonces el consejo estándar es no usar staticmethoden absoluto. Por lo general, son más útiles como funciones de nivel de módulo, en cuyo caso su problema no es un problema. classmethod, por otro lado ...
Benjamin Hodgson
1
@poorsod: Sí, soy consciente de esa alternativa. Sin embargo, en el código real donde encontré este problema, hacer que la función sea un método estático en lugar de ponerla a nivel de módulo tiene más sentido que en el ejemplo simple utilizado en mi pregunta.
Martineau

Respuestas:

180

staticmethodAparentemente, los objetos tienen un __func__atributo que almacena la función original sin procesar (tiene sentido que tuvieran que hacerlo). Entonces esto 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

Por otro lado, aunque sospechaba que un objeto de método estático tenía algún tipo de atributo que almacenaba la función original, no tenía idea de los detalles. En el espíritu de enseñar a alguien a pescar en lugar de darle un pez, esto es lo que hice para investigar y descubrirlo (un C&P de mi sesión de 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 similares de excavación en una sesión interactiva (dir es muy útil) a menudo pueden resolver este tipo de preguntas muy rápidamente.

Ben
fuente
Buena actualización ... Estaba a punto de preguntarle cómo lo sabía, ya que no lo veo en la documentación, lo que me pone un poco nervioso por usarlo porque podría ser un "detalle de implementación".
Martineau
Después de leer más, veo que ese __func__es solo otro nombre im_funcy se agregó a Py 2.6 para la compatibilidad con versiones anteriores de Python 3.
Martineau
2
Ya veo, así que técnicamente no está documentado en este contexto.
Martineau
1
@AkshayHazari El __func__atributo de un método estático le proporciona una referencia a la función original, exactamente como si nunca hubiera utilizado el staticmethoddecorador. Entonces, si su función requiere argumentos, deberá pasarlos cuando llame __func__. El mensaje de error parece que no le ha dado ningún argumento. Si stat_funcen el ejemplo de esta publicación tomara dos argumentos, usaría_ANS = stat_func.__func__(arg1, arg2)
Ben el
1
@AkshayHazari No estoy seguro de entender. Pueden ser variables, solo tienen que ser variables dentro del alcance: definidas anteriormente en el mismo alcance donde se define la clase (a menudo global) o definidas anteriormente dentro del alcance de la clase (en stat_funcsí misma es una variable). ¿Quiso decir que no puede usar atributos de instancia de la clase que está definiendo? Eso es cierto, pero no sorprende; ¡No tenemos una instancia de la clase, ya que todavía estamos definiendo la clase! Pero de todos modos, me refería a ellos como sustitutos de cualquier argumento que quisieras pasar; podrías usar literales allí.
Ben
24

Esta es la forma en que prefiero:

class Klass(object):

    @staticmethod
    def stat_func():
        return 42

    _ANS = stat_func.__func__()

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

Prefiero esta solución Klass.stat_func, debido al principio DRY . Me recuerda la razón porsuper() la cual hay una nueva en Python 3 :)

Pero estoy de acuerdo con los demás, por lo general, la mejor opción es definir una función de nivel de módulo.

Por ejemplo, con la @staticmethodfunción, la recursión puede no verse muy bien (necesitaría romper el principio DRY llamando al Klass.stat_funcinterior Klass.stat_func). Eso es porque no tiene referencia al selfmétodo estático interno. Con la función de nivel de módulo, todo se verá bien.

Jan Vorcak
fuente
Si bien estoy de acuerdo en que usar self.__class__.stat_func()métodos regulares tiene ventajas (DRY y todo eso) sobre el uso Klass.stat_func(), ese no fue realmente el tema de mi pregunta; de hecho, evité usar el primero para no nublar el problema de la inconsistencia.
martineau
1
No es realmente un problema de DRY per se. self.__class__es mejor porque si una subclase anula stat_func, entonces Subclass.methodllamará a la subclase stat_func. Pero para ser completamente honesto, en esa situación es mucho mejor usar un método real en lugar de uno estático.
asmeurer
@asmeurer: No puedo usar un método real porque todavía no se han creado instancias de la clase, no pueden serlo ya que la clase en sí misma aún no se ha definido completamente.
Martineau
Quería una forma de llamar al método estático para mi clase (que se hereda; por lo tanto, el padre realmente tiene la función) y esto parece mejor por mucho (y aún funcionará si lo anulo más adelante).
whitey04
11

¿Qué hay de inyectar el atributo de clase después de la definición de clase?

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
fuente
1
Esto es similar a mis primeros intentos de evitarlo, pero preferiría algo que estuviera dentro de la clase ... en parte porque el atributo involucrado tiene un guión bajo y es privado.
Martineau
11

Esto se debe a que el método estático es un descriptor y requiere una búsqueda de atributos a nivel de clase para ejercer el protocolo del descriptor y obtener el verdadero invocable.

Del código fuente:

Se puede invocar en la clase (p C.f(). Ej. ) O en una instancia (p C().f(). Ej. ); la instancia se ignora a excepción de su clase.

Pero no directamente desde dentro de la clase mientras se está definiendo.

Pero como mencionó un comentarista, este no es realmente un diseño "pitónico" en absoluto. Simplemente use una función de nivel de módulo en su lugar.

Keith
fuente
Como dije en mi pregunta, entiendo por qué el código original no funcionó. ¿Puede explicar (o proporcionar un enlace a algo que lo haga) por qué se considera antipático?
Martineau
El método estático requiere que el propio objeto de clase funcione correctamente. Pero si lo llama desde dentro de la clase a nivel de clase, la clase no está realmente completamente definida en ese punto. Entonces no puedes hacer referencia a él todavía. Está bien llamar al método estático desde fuera de la clase, después de que esté definido. Pero " Pythonic " no está realmente bien definido, al igual que la estética. Les puedo decir que mi primera impresión de ese código no fue favorable.
Keith el
¿Impresión de qué versión (o ambas)? ¿Y por qué, específicamente?
Martineau
Solo puedo adivinar cuál es tu objetivo final, pero me parece que quieres un atributo dinámico de nivel de clase. Esto parece un trabajo para metaclases. Pero, una vez más, puede haber una forma más simple u otra forma de ver el diseño que elimina y simplifica sin sacrificar la funcionalidad.
Keith
3
No, lo que quiero hacer es en realidad bastante simple, que es factorizar un código común y reutilizarlo, tanto para crear un atributo de clase privada en el momento de la creación de la clase, como más tarde dentro de uno o más métodos de clase. Este código común no tiene uso fuera de la clase, por lo que, naturalmente, quiero que sea parte de él. Usar una metaclase (o un decorador de clase) funcionaría, pero eso parece excesivo para algo que debería ser fácil de hacer, en mi humilde opinión.
Martineau
8

¿Qué hay de esta solución? No se basa en el conocimiento de la @staticmethodimplementación del decorador. La clase interna StaticMethod juega como un contenedor de funciones de inicialización 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
fuente
2
+1 por creatividad, pero ya no me preocupa usar __func__ porque ahora está documentado oficialmente (consulte la sección Otros cambios de idioma de las novedades de Python 2.7 y su referencia al problema 5982 ). Su solución es aún más portátil, ya que probablemente también funcionaría en versiones de Python anteriores a 2.6 (cuando __func__se introdujo por primera vez como sinónimo de im_func).
Martineau
Esta es la única solución que funciona con Python 2.6.
benselme
@benselme: No puedo verificar su reclamo porque no tengo Python 2.6 instalado, pero seria la única duda ...
Martineau