Python Functions Aren't What You Think
It took me years to figure this out. Look at this:
- >>> def to_percent(fraction):
- ... return str(round(fraction * 100)) + '%'
- ...
- >>> to_percent(17/31)
- '55%'
- >>> # Python also sets attributes like __name__, etc.
- ... to_percent.__name__
- '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:
- >>> nums = [1, -7, 3, 12, -3]
- >>> positives = filter(lambda num: num > 0, nums)
- >>> print(list(positives))
- [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:
- >>> to_percent = lambda fraction: str(round(fraction * 100)) + '%'
- # __name__ can be set, like any other attribute.
- >>> to_percent.__name__ = 'to_percent'
This gives us a to_percent function which is
indistinguishable from one created the normal way.
- >>> to_percent(17/31)
- '55%'
- >>> to_percent.__name__
- '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:
- >>> numbers = ["7", "12", "-3", "123"]
- >>> max(numbers)
- '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:
- def max_by_int_value(items):
- # For simplicity, assume len(items) > 0
- biggest = items[0]
- for item in items[1:]:
- if int(item) > int(biggest):
- biggest = item
- 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:
- >>> numbers = ["7", "12", "-3", "123"]
- >>> max(numbers, key=int)
- '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:
- def max_by_key(items, key):
- biggest = items[0]
- for item in items[1:]:
- if key(item) > key(biggest):
- biggest = item
- return biggest
Notice:
- The signature includes a variable called
key. On the "def" line, it looks like any other variable. - The middle line, "
if key(item) > key(biggest)", uses that variable... invoking it as a function.
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?