Atributos de la función Python: usos y abusos [cerrado]

195

No muchos conocen esta característica, pero las funciones (y métodos) de Python pueden tener atributos . Mirad:

>>> def foo(x):
...     pass
...     
>>> foo.score = 10
>>> dir(foo)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__get__', '__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name', 'score']
>>> foo.score
10
>>> foo.score += 1
>>> foo.score
11

¿Cuáles son los posibles usos y abusos de esta función en Python? Un buen uso que conozco es el uso que hace PLY de la cadena de documentación para asociar una regla de sintaxis con un método. ¿Pero qué pasa con los atributos personalizados? ¿Hay buenas razones para usarlos?

Eli Bendersky
fuente
3
Echa un vistazo a PEP 232 .
user140352
2
¿Es esto muy sorprendente? En general, los objetos Python admiten atributos ad-hoc. Por supuesto, algunos no, particularmente aquellos con tipo incorporado. Para mí, aquellos que no lo apoyan parecen ser las excepciones, no la regla.
allyourcode
3
Una aplicación en Django: Personalice la lista de cambios de administrador
Grijesh Chauhan
2
@GrijeshChauhan ¡Llegué a esta pregunta después de ver estos documentos!
Alexander Suraphel
55
Lástima que esto esté cerrado, quería agregar que puede adjuntar cualquier excepción personalizada que pueda generar la función, para proporcionar un acceso fácil al capturarla en el código de llamada. Proporcionaría un ejemplo ilustrativo, pero es mejor hacerlo en una respuesta.
Will Hardy

Respuestas:

153

Normalmente uso atributos de funciones como almacenamiento para anotaciones. Supongamos que quiero escribir, al estilo de C # (lo que indica que cierto método debería ser parte de la interfaz del servicio web)

class Foo(WebService):
    @webmethod
    def bar(self, arg1, arg2):
         ...

entonces puedo definir

def webmethod(func):
    func.is_webmethod = True
    return func

Luego, cuando llega una llamada de servicio web, busco el método, compruebo si la función subyacente tiene el atributo is_webmethod (el valor real es irrelevante) y rechazo el servicio si el método está ausente o no se debe llamar a través de la web.

Martin v. Löwis
fuente
2
¿Crees que hay desventajas en esto? Por ejemplo, ¿qué pasa si dos bibliotecas intentan escribir el mismo atributo ad-hoc?
allyourcode
18
Estaba pensando en hacer exactamente esto. Entonces me detuve. "¿Es una mala idea?" Me preguntaba. Entonces, me acerqué a SO. Después de un poco de torpeza, encontré esta pregunta / respuesta. Todavía no estoy seguro si esta es una buena idea.
allyourcode
77
Este es definitivamente el uso más legítimo de los atributos de función de todas las respuestas (a partir de noviembre de 2012). La mayoría (si no todas) las otras respuestas usan atributos de función como reemplazo de variables globales; sin embargo, NO eliminan el estado global, que es exactamente el problema con las variables globales. Esto es diferente, porque una vez que se establece el valor, no cambia; Es constante. Una buena consecuencia de esto es que no se encuentra con problemas de sincronización, que son inherentes a las variables globales. Sí, puede proporcionar su propia sincronización, pero ese es el punto: no es seguro automáticamente.
allyourcode
De hecho, digo, siempre que el atributo no cambie el comportamiento de la función en cuestión, es bueno. Compare con.__doc__
Dima Tisnek
Este enfoque también se puede utilizar para adjuntar una descripción de salida a la función decorada, que falta en Python 2. *.
Juh_
125

Los he usado como variables estáticas para una función. Por ejemplo, dado el siguiente código C:

int fn(int i)
{
    static f = 1;
    f += i;
    return f;
}

Puedo implementar la función de manera similar en Python:

def fn(i):
    fn.f += i
    return fn.f
fn.f = 1

Esto definitivamente caería en el extremo de "abusos" del espectro.

mipadi
fuente
2
Interesante. ¿Hay otras formas de implementar variables estáticas en python?
Eli Bendersky
44
-1, esto se implementaría con un generador en python.
124
Esa es una razón bastante pobre para rechazar esta respuesta, que demuestra una analogía entre C y Python, no aboga por la mejor manera posible de escribir esta función en particular.
Robert Rossney, el
3
@RobertRossney Pero si los generadores son el camino a seguir, entonces este es un mal uso de los atributos de función. Si es así, entonces esto es un abuso. Sin embargo, no estoy seguro de si votar a favor de los abusos, ya que la pregunta también los pide: P
allyourcode el
1
Definitivamente parece un abuso, según PEP 232, gracias @ user140352, y el comentario de hop y esta respuesta SO
hobs
52

Puede hacer objetos a la manera de JavaScript ... No tiene sentido pero funciona;)

>>> def FakeObject():
...   def test():
...     print "foo"
...   FakeObject.test = test
...   return FakeObject
>>> x = FakeObject()
>>> x.test()
foo
difunto
fuente
36
+1 Un buen ejemplo de abuso de esta función, que es una de las cosas que se hizo la pregunta.
Michael Dunn
1
¿Cómo es esto diferente de la respuesta de mipadi? Parece ser lo mismo, excepto que en lugar de un int, el valor del atributo es una función.
allyourcode
es def test()realmente necesario?
Keerthana Prabhakaran
15

Los uso con moderación, pero pueden ser bastante convenientes:

def log(msg):
   log.logfile.write(msg)

Ahora puedo usar logtodo mi módulo y redirigir la salida simplemente configurando log.logfile. Hay muchas otras formas de lograrlo, pero este es liviano y muy simple. Y aunque olía raro la primera vez que lo hice, he llegado a creer que huele mejor que tener una logfilevariable global .

Robert Rossney
fuente
77
Re olfato: sin embargo, esto no elimina el archivo de registro global. Simplemente lo oculta en otra función de registro global.
allyourcode
2
@allyourcode: Pero puede ayudar a evitar conflictos de nombres si tiene que tener un montón de archivos de registro globales para diferentes funciones en el mismo módulo.
firegurafiku
11

Los atributos de función se pueden usar para escribir cierres ligeros que envuelvan el código y los datos asociados juntos:

#!/usr/bin/env python

SW_DELTA = 0
SW_MARK  = 1
SW_BASE  = 2

def stopwatch():
   import time

   def _sw( action = SW_DELTA ):

      if action == SW_DELTA:
         return time.time() - _sw._time

      elif action == SW_MARK:
         _sw._time = time.time()
         return _sw._time

      elif action == SW_BASE:
         return _sw._time

      else:
         raise NotImplementedError

   _sw._time = time.time() # time of creation

   return _sw

# test code
sw=stopwatch()
sw2=stopwatch()
import os
os.system("sleep 1")
print sw() # defaults to "SW_DELTA"
sw( SW_MARK )
os.system("sleep 2")
print sw()
print sw2()

1.00934004784

2.00644397736

3.01593494415

Kevin Little
fuente
3
¿Por qué funciones push cuando tenemos clases a mano? Y no olvidemos que las clases pueden emular una función.
muhuk
1
también time.sleep(1)es mejor queos.system('sleep 1')
Boris Gorelik
3
@bgbg Cierto, aunque este ejemplo no se trata de dormir.
allyourcode
Esto definitivamente es un abuso; El uso de funciones aquí es totalmente gratuito. Muhuk tiene toda la razón: las clases son una mejor solución.
allyourcode
1
También preguntaría: "¿Cuál es la ventaja de esto sobre una clase?" para contrarrestar la desventaja de que esto no sea tan obvio para muchos programadores de Python.
cjs
6

He creado este decorador auxiliar para configurar fácilmente los atributos de la función:

def with_attrs(**func_attrs):
    """Set attributes in the decorated function, at definition time.
    Only accepts keyword arguments.
    E.g.:
        @with_attrs(counter=0, something='boing')
        def count_it():
            count_it.counter += 1
        print count_it.counter
        print count_it.something
        # Out:
        # >>> 0
        # >>> 'boing'
    """
    def attr_decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            return fn(*args, **kwargs)

        for attr, value in func_attrs.iteritems():
            setattr(wrapper, attr, value)

        return wrapper

    return attr_decorator

Un caso de uso es crear una colección de fábricas y consultar el tipo de datos que pueden crear en un meta nivel de función.
Por ejemplo (muy tonto):

@with_attrs(datatype=list)
def factory1():
    return [1, 2, 3]

@with_attrs(datatype=SomeClass)
def factory2():
    return SomeClass()

factories = [factory1, factory2]

def create(datatype):
    for f in factories:
        if f.datatype == datatype:
            return f()
    return None
DiogoNeves
fuente
¿Cómo ayuda el decorador? ¿Por qué no simplemente establecer factory1.datatype=listjusto debajo de la declaración como decoradores de estilo antiguo?
Ceasar Bautista
2
2 diferencias principales: estilo, múltiples atributos son más fáciles de configurar. Definitivamente, puede establecerlo como un atributo, pero en mi opinión se detalla con múltiples atributos y también tiene la oportunidad de extender el decorador para un procesamiento posterior (como tener valores predeterminados que se definen en un lugar en lugar de todos los lugares que usan la función o tener que llamar a una función adicional después de establecer los atributos). Hay otras formas de lograr todos estos resultados, solo encuentro que esto es más limpio, pero feliz de cambiar de opinión;)
DiogoNeves
Actualización rápida: con Python 3 debe usar en items()lugar de iteritems().
Scott significa
4

A veces uso un atributo de una función para almacenar en caché los valores ya calculados. También puede tener un decorador genérico que generalice este enfoque. ¡Tenga en cuenta los problemas de concurrencia y los efectos secundarios de tales funciones!


fuente
¡Me gusta esta idea! Un truco más común para almacenar en caché los valores calculados es usar un dict como el valor predeterminado de un atributo que la persona que llama nunca tiene la intención de proporcionar, ya que Python evalúa que solo una vez al definir la función, puede almacenar datos allí y hacer que se pegue alrededor. Si bien el uso de atributos de función puede ser menos obvio, me parece mucho menos hacky.
Soren Bjornstad
1

Siempre supuse que la única razón por la que esto era posible era porque había un lugar lógico para colocar una cadena de documentos u otras cosas similares. Sé que si lo usara para algún código de producción, confundiría a la mayoría de los que lo leen.

Dale Reidy
fuente
1
Estoy de acuerdo con su punto principal sobre lo más probable que esto sea confuso, pero re docstrings: Sí, pero ¿por qué las funciones tienen atributos AD-HOC? Podría haber un conjunto fijo de atributos, uno para mantener la cadena de documentación.
allyourcode
@allyourcode Tener el caso general en lugar de casos específicos ad-hoc diseñados en el lenguaje simplifica las cosas y aumenta la compatibilidad con versiones anteriores de Python. (Por ejemplo, el código que manipula sistemas / docstrings seguirá funcionando con una versión de Python que no hace cadenas de documentación, siempre y cuando se maneja el caso en que no existe el atributo.)
CJS