This is Noah Gift's Coding Blog. I only talk about coding and technical stuff here, including Artificial Intelligence when I can.

Tuesday, April 28, 2009

Python Functional Programming Antipatterns: When Closures Can Be A Solution In Search of A Problem (PART 1)

One of the things I don't like about closures [1](via nested functions) is how they obscure intent in code. For example, if you just want to retain state why use a closure if you could just use a class? Sure a closure sounds cooler, but a class or a regular group of functions is often more flexible and readable than a closure.


Example 1: Simple Persistent State
Closure That Stores State: (1A)



In [39]: def outer():
....: x = 1
....: def inner():
....: return x
....: return inner
....:

In [40]: func = outer()

In [41]: func
Out[41]:

In [42]: func()
Out[42]: 1



Class That Stores State (1B)



In [46]: class State(object):
....: def __init__(self):
....: self.x = 1
....: def func(self):
....: return self.x
....:
....:

In [47]: func = State().func

In [48]: func()
Out[48]: 1





Score: +1 Class
Summary: If you simply want to persistent state, why use a closure that
has an odd signature, when you can simple use a class?


Example 2: Flat is better than nested, closures taken to an extreme with multiple nesting

Nested function w/ function that operates on state (2A)




In [18]: def way_outer():
....: wo = 1
....: def inner():
....: i = 2
....: def way_inner():
....: return i + wo
....: return way_inner
....: return inner
....:

In [19]: out = way
way_inner way_outer

In [19]: out = way_outer()

In [20]: out
Out[20]:

In [21]: out()
Out[21]:

In [22]: way_way_out = out()

In [23]: way_way_out()
Out[23]: 3



Class functions that operate on state (2B)



In [24]: class Addition(object):
....: def __init__(self):
....: self.x = 1
....: self.y = 2
....: def add(self):
....: return self.x + self.y
....:
....:

In [25]: a = Addition().add

In [26]: a
Out[26]: [bound method Addition.add of __main__.Addition object at 0x78db70]

In [27]: a()
Out[27]: 3




Score: +2 Class
Summary: You might be thinking "duh" with this example, but yes, that is exactly the point! Why nest things if you don't have to? Especially because of the fact that if someone looks at your highly nested closure they will go WTF as they should. Why make things more complex then they need to be? Just because you can use closures doesn't mean you should or it makes your code intuitive and readable.

Example 3: Delaying Execution of a Function
Delayed execution of a function using closures (3A)



In [29]: def outer():
....: def inner(x):
....: return x + 1
....: return inner
....:

In [30]: func = outer()

In [31]: func(3)
Out[31]: 4



Delayed execution of a function using class: Example (3B)


In [1]: class DelayCall(object):
...: def __init__(self, x):
...: self.x = x
...: def logic(self):
...: return self.x + 1
...: def delay(self):
...: return self.logic
...:
...:

In [3]: d = DelayCall(3)

In [4]: func = d.delay()

In [5]: func()
Out[5]: 4




Delayed execution of a function that takes args using a lambda in a class: Example (3C)


In [19]: class LambdaDelayCall(object):
....: def logic(self, x):
....: return x + 1
....: def delay(self):
....: return (lambda x: self.logic(x))
....:
....:

In [20]: l = LambdaDelayCall()

In [21]: func = l.delay()

In [22]: func(5)
Out[22]: 6



Delayed execution of a function that takes args using a lambda in a class w/ __call__: Example (3D)


n [19]: class LambdaDelayCall(object):
....: def logic(self, x):
....: return x + 1
....: def __call__(self):
....: return (lambda x: self.logic(x))
....:
....:

In [37]: l = LambdaDelayCall()

In [40]: func = l()

In [41]: func(3)
Out[41]: 4





Score: +3 Class
Summary: While 3A is shorter, I think it is much less clear then 3B, 3C or 3D, the class examples. I think of a closure in terms of an outer function that needs to operate on an inner function, such as in the case of a decorator. Using a closure just to delay execution of a function doesn't seem right to me, and I feel like it obscures the code.

Conclusion:

Closures are useful when you want to modify the state of inner function and return, as in the case of decorators. Lambdas, from Learning Python 3rd edition, " are often used as a way to inline a function definition, or to defer execution of a piece of code.". While closures are useful and powerful, with power comes responsibility. [1] Nested functions aren't that clear, try a class or maybe you don't even need a closure?. Make sure you Python for good, not evil. Stay tuned for the next installment.

[1] Added note about "nested functions" 04/29/2009


References:
Bruce Eckel on Decorators
Zen of Python
Functools Partial
Dive Into Python: Lambda

6 comments:

MononcQc said...

Those examples with lambda are relatively dishonnest given Lambdas are anonymous functions, which is pretty much what closures can be about.

Wybiral said...

Yeah, in those last snippets

"lambda x: self.logic(x)"

is a closure (and "self" is state being captured in it). So, you're still using closures in those...

The reason closures are more convenient in *some* situations is that you don't have to make a ton of helper classes to capture that state, it just happens for you.

And in many cases (especially simple ones, like your need to capture "self") it would be overkill to implement some kind of class or other infrastructure for that (like having to pass it around everywhere to save it).

You could have used something like:

def outer(x):
    return lambda y: x + y

That is a closure, it's still very readable, and it's a lot less code than having to write some class just for that functionality.

Noah Gift said...

@Wybiral
I just added a note that I meant in the case of nested functions, I think a class is cleaner.

I do like your closure syntax though, as it gets rid of a nested function.

iny said...

What about generators?

eswald said...

Some of the ipython output gets lost (particularly the "<function ...>" bits) due to lack of escaping. It probably doesn't obscure your point much, but it's particularly distracting in example 2.

Noah Gift said...

@iny

Generators are incredibly useful, especially went you don't want to pull everything into memory all at once. David Beazely has the definitive examples on using generators: http://www.dabeaz.com/generators/, but even he cautions that they are hard to debug.

My advice is to completely understand all of the advanced features of Python, but to be careful in using them. Ultimately readable code is the ultimate form of Python prowess, not tricky code....that is what Perl is for :)