Monday, May 21, 2007

Python: islambda()

The Python inspect module provides functions that determine whether objects are methods, functions, classes, modules, etc. However, there is no method that tells you whether something is a lambda expression. The isfunction() is probably close enough for most applications, but believe it or not, I recently encountered a case where it made sense to issue a warning if an argument was a lambda expression.

Here is a python function that determines whether or not its argument is a lambda expression:

import inspect
def islambda(f):
return inspect.isfunction(f) and \
f.__name__ == (lambda: True).__name__

Currently, the __name__ of anonymous functions created by lambda is "<lambda>", so I could have just hard-coded that string into the comparison. But I chose to use the (lambda: True).__name__ expression instead just in case python uses a different name for lambda expressions in the future.

This function works as expected in all common cases:

>>> islambda(lambda: 1)
True
>>> islambda(islambda)
False
>>> islambda(globals)
False
>>> islambda(str)
False
>>> islambda(str.join)
False
>>> islambda("".join)
False

The only case that I am aware of where it will not work is if you carefully craft a function with the name "<lambda>":

import new
>>> x = new.function(
compile("print 'Hello World!'", "<string>", "exec"),
{}, '<lambda>')
>>> islambda(x)
True

Consider yourself warned. :)

In case you are curious, the application I was working on had a method that took a callable as an argument and held a weak reference to it. I would have loved to have been able to issue a warning anytime a callable was passed that would "immediately" be garbage collected before anything useful was done, but that is a non-trivial condition to detect (hint: it involves reading the programmer's mind). But there is a common subset of that error case that is relatively easy to detect: callers passing their only reference to a lambda expression. That case can be trivially detected using the islambda() function described above along with the sys.getrefcount() function like so:

import sys
from warnings import warn
...
def myfunc(f):
if sys.getrefcount(f) == 3 and islambda(f):
warn('f is too short-lived to be useful', stacklevel=2)
...

Since it is not obvious, I should point out that (in this example) a reference count of 3 indicates that myfunc()'s caller holds no references to the callable f. The reason is that sys.getrefcount() will hold one reference, the name f is bound to one reference, and there is a temporary reference held by the python interpreter across the call to myfunc(), so if sys.getrefcount() returns 3, we know those are the only three references.

Incidentally, the fact that islambda() erroniously identifies a function with the same "<lambda>" as a lambda expression is inconsequential for my stated purpose: if the crafted function has no other references, I want to issue a warning just the same as if it had truly been a lambda expression.

Which brings me back to isfunction(). It turns out, not surprisingly, that isfunction() is sufficient for my needs since a function with only 3 references has, by definition, no external references. In the end, I didn't actually use my islambda() function and went with isfunction() for my application instead:

import inspect
import sys
from warnings import warn
...
def myfunc(f):
if sys.getrefcount(f) == 3 and inspect.isfunction(f):
warn('f is too short-lived to be useful', stacklevel=2)
...

This handles both lambda expressions and functions dynamically created using the new module.

No comments: