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?