Atributos de la función Python – usos y abusos

No muchos conocen esta característica, pero las funciones (y los 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 característica en Python? Un buen uso que conozco es el uso que hace PLY de la cadena de documentos para asociar una regla de syntax con un método. Pero ¿qué pasa con los atributos personalizados? ¿Hay buenas razones para usarlos?

Normalmente uso atributos de funciones como almacenamiento para anotaciones. Supongamos que quiero escribir, en el estilo de C # (lo que indica que un determinado método debe formar 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 está destinado a ser llamado a través de la web.

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.

Puede hacer objetos de forma JavaScript … No tiene sentido pero funciona;)

 >>> def FakeObject(): ... def test(): ... print "foo" ... FakeObject.test = test ... return FakeObject >>> x = FakeObject() >>> x.test() foo 

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

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

Ahora puedo usar log todo mi módulo y redirigir la salida simplemente configurando log.logfile . Hay muchas otras formas de lograrlo, pero esta es liviana y simple. Y aunque olía raro la primera vez que lo hice, llegué a creer que huele mejor que tener una variable global de logfile .

Los atributos de función se pueden usar para escribir cierres ligeros que encierran código y 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

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. ¡Sea consciente de los problemas de concurrencia y los efectos secundarios de tales funciones!

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. Eg: @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 

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