Python function attributes - uses and abuses
PythonFunctionAttributesPython Problem Overview
Not many are aware of this feature, but Python's functions (and methods) can have attributes. Behold:
>>> 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
What are the possible uses and abuses of this feature in Python ? One good use I'm aware of is PLY's usage of the docstring to associate a syntax rule with a method. But what about custom attributes ? Are there good reasons to use them ?
Python Solutions
Solution 1 - Python
I typically use function attributes as storage for annotations. Suppose I want to write, in the style of C# (indicating that a certain method should be part of the web service interface)
class Foo(WebService):
@webmethod
def bar(self, arg1, arg2):
...
then I can define
def webmethod(func):
func.is_webmethod = True
return func
Then, when a webservice call arrives, I look up the method, check whether the underlying function has the is_webmethod attribute (the actual value is irrelevant), and refuse the service if the method is absent or not meant to be called over the web.
Solution 2 - Python
I've used them as static variables for a function. For example, given the following C code:
int fn(int i)
{
static f = 1;
f += i;
return f;
}
I can implement the function similarly in Python:
def fn(i):
fn.f += i
return fn.f
fn.f = 1
This would definitely fall into the "abuses" end of the spectrum.
Solution 3 - Python
You can do objects the JavaScript way... It makes no sense but it works ;)
>>> def FakeObject():
... def test():
... print "foo"
... FakeObject.test = test
... return FakeObject
>>> x = FakeObject()
>>> x.test()
foo
Solution 4 - Python
I use them sparingly, but they can be pretty convenient:
def log(msg):
log.logfile.write(msg)
Now I can use log
throughout my module, and redirect output simply by setting log.logfile
. There are lots and lots of other ways to accomplish that, but this one's lightweight and dirt simple. And while it smelled funny the first time I did it, I've come to believe that it smells better than having a global logfile
variable.
Solution 5 - Python
Function attributes can be used to write light-weight closures that wrap code and associated data together:
#!/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
Solution 6 - Python
I've created this helper decorator to easily set function attributes:
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
A use case is to create a collection of factories and query the data type they can create at a function meta level.
For example (very dumb one):
@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
Solution 7 - Python
Sometimes I use an attribute of a function for caching already computed values. You can also have a generic decorator that generalizes this approach. Be aware of concurrency issues and side effects of such functions!
Solution 8 - Python
I was always of the assumption that the only reason this was possible was so there was a logical place to put a doc-string or other such stuff. I know if I used it for any production code it'd confuse most who read it.