Python Functions Aren't What You Think

It took me years to figure this out. Look at this:

  1. >>> def to_percent(fraction):
  2. ... return str(round(fraction * 100)) + '%'
  3. ...
  4. >>> to_percent(17/31)
  5. '55%'
  6. >>> # Python also sets attributes like __name__, etc.
  7. ... to_percent.__name__
  8. 'to_percent'

I bet you think this creates a function called "to_percent". That's reasonable. After all, you can write code like x = to_percent(0.75) to put the string "75%" in the variable x. Of course it's creating a function called to_percent.

But.

There's another way to look at it, which you may find strange, even hard to believe. That other perspective is this: Python functions cannot have names. In this world view, every function is a nameless, anonymous object. Code like "def to_percent(numbers)" creates a nameless function object, then stores it in a variable called "to_percent".

Remarkably, this turns out to be just as valid.

You may know Python lets you create small, nameless functions via lambda:

  1. >>> nums = [1, -7, 3, 12, -3]
  2. >>> positives = filter(lambda num: num > 0, nums)
  3. >>> print(list(positives))
  4. [1, 3, 12]

Lambdas exist for the sole purpose of creating functions with no name. Yet you can completely fake the definition of to_percent using a lambda:

  1. >>> to_percent = lambda fraction: str(round(fraction * 100)) + '%'
  2. # __name__ can be set, like any other attribute.
  3. >>> to_percent.__name__ = 'to_percent'

This gives us a to_percent function which is indistinguishable from one created the normal way.

  1. >>> to_percent(17/31)
  2. '55%'
  3. >>> to_percent.__name__
  4. 'to_percent'

In this new way of thinking, when you write "def to_percent(fraction)", you're actually creating an anonymous function anyway. Python just stores it in a variable called to_percent.

We're talking about mental models here. You can adopt one mental model or the other, just like you'd put on different hats, switching as needed. "def to_percent() creates a function called to_percent" is one mental model; "all functions in Python are nameless" is another.

Which is more accurate? It doesn't matter. Provided both are accurate enough - and they both are - what matters is which mental model is more useful. Meaning: does thinking this way let you accomplish what you want, easily and effectively.

As a mental model, "Python functions are nameless" demystifies certain patterns. Consider this situation:

  1. >>> numbers = ["7", "12", "-3", "123"]
  2. >>> max(numbers)
  3. '7'

max considers the string "7" to be greater than "123", for the same reason "g" comes after "abc" alphabetically. What if I want the maximum numeric value, though? Well, max is a simple algorithm... maybe I can just write my own:

  1. def max_by_int_value(items):
  2. # For simplicity, assume len(items) > 0
  3. biggest = items[0]
  4. for item in items[1:]:
  5. if int(item) > int(biggest):
  6. biggest = item
  7. return biggest

Sure enough, max_by_int_value(numbers) returns "123". And the interesting line is in the middle, "if int(item) > int(biggest)". Because it turns out I can make the built-in max behave this way too:

  1. >>> numbers = ["7", "12", "-3", "123"]
  2. >>> max(numbers, key=int)
  3. '123'

Can you guess what's happening?

The variable key stores a function object (or more generally, a callable - i.e. something that can be called like a function). It helps to imagine our own generic version of max:

  1. def max_by_key(items, key):
  2. biggest = items[0]
  3. for item in items[1:]:
  4. if key(item) > key(biggest):
  5. biggest = item
  6. return biggest

Notice:

I've found many experienced, smart developers have trouble reasoning about code like max_by_key. Those who understand it read an expression like key(item), then say to yourself "take whatever function is stored in 'key', and call it, passing 'item' as an argument". When stuck in a different mental model, one can have a hard time thinking this way - happens to all of us.

Until we learn to get un-stuck.

And that's what's really important. Our mastery of programming depends on our mental flexibilty. A mental model is a tool, like a hammer; that tool has its uses, and also its limits. Sometimes you need to put down the hammer and get a screwdriver.

What mental models are in your toolbox? How can you add more?


You might enjoy the Powerful Python book because of the new mental models it gives you.