mirror of
https://github.com/andwati/andwati.com.git
synced 2026-10-18 17:32:52 +00:00
242 lines
7.8 KiB
Markdown
242 lines
7.8 KiB
Markdown
+++
|
|
title = "Stateful Callbacks in Python"
|
|
description = "Stateful callbacks in Python."
|
|
date = 2017-07-10T07:54:00-08:00
|
|
[taxonomies]
|
|
tags = ["Python", "callbacks"]
|
|
+++
|
|
If you're unfamiliar with what a callback is, don't worry, we can sort that out
|
|
quickly. If callbacks are old hat for you you might want to skip to
|
|
[the interesting bit](#stateful-callbacks).
|
|
|
|
Simply put, a callback is a function that is passed as an argument to
|
|
another function which may execute it.
|
|
|
|
<!-- more -->
|
|
|
|
Take, for example, these functions:
|
|
|
|
```python
|
|
def bar():
|
|
return "I'm the callback!"
|
|
|
|
def foo(func):
|
|
return func()
|
|
```
|
|
|
|
If we call `foo` like this
|
|
|
|
```python
|
|
>>> foo(bar)
|
|
"I'm the callback!"
|
|
```
|
|
|
|
then `bar` is a callback.
|
|
|
|
## Why Should I Use A Callback?
|
|
|
|
There are many reasons to use callbacks. For me, the most compelling is
|
|
customization. Let's take a look at a Python built-in as an example. Say we
|
|
have a list of users as dictionaries with a `name` and an `age`:
|
|
|
|
```python
|
|
users = [
|
|
dict(age=77, name='John Cleese'),
|
|
dict(age=74, name='Eric Idle'),
|
|
]
|
|
```
|
|
|
|
Imagine that we want to sort our users. If we had just a list of ages or a list
|
|
of names we could easily do this with the built-in `sorted` function, but by
|
|
default Python has no idea how to compare our dictionaries during sorting.
|
|
|
|
```python
|
|
>>> sorted(users)
|
|
Traceback (most recent call last):
|
|
File "<stdin>", line 1, in <module>
|
|
TypeError: unorderable types: dict() < dict()
|
|
```
|
|
|
|
Should it sort by `age`? By `name`? We need to tell Python how this should be
|
|
done. Fortunately Python provides and the `sorted` function has a keyword
|
|
argument called `key` that takes, you guessed it, a callback. Let's create some
|
|
of our own!
|
|
|
|
```python
|
|
def by_age(user):
|
|
return user['age']
|
|
|
|
def by_name(user):
|
|
return user['name']
|
|
```
|
|
|
|
Armed with these callbacks we can sort our users.
|
|
|
|
```python
|
|
>>> sorted(users, key=by_age)
|
|
[{'age': 74, 'name': 'Michael Palin'}, {'age': 77, 'name': 'John Cleese'}]
|
|
>>> sorted(users, key=by_name)
|
|
[{'age': 77, 'name': 'John Cleese'}, {'age': 74, 'name': 'Michael Palin'}]
|
|
```
|
|
|
|
Since the `sorted` function takes a callback for the `key` argument we are free
|
|
to customize its behavior. All we have to do is define a function that returns
|
|
the key we intend to sort by and as long as that's an orderable type Python
|
|
will take care of the rest.
|
|
|
|
## What Does It Mean to Have State?
|
|
|
|
So, by now we have something of an idea of what callbacks are, how we can use
|
|
them, and why, but what's the point of state? State is most easily described as
|
|
a memory of prior events. This is the core of what every program does and we
|
|
use it all the time, even if we don't realize it. Heck, even saving a variable
|
|
involves keeping track of state.
|
|
|
|
```python
|
|
>>> baz = 1 # The Python interpreter is now tracking the state of 'baz'.
|
|
>>> print(baz) # We can recall that state at a later point.
|
|
1
|
|
```
|
|
|
|
Basically, we need state if we care to remember what happened previously so
|
|
that we can make decisions about what to do next.
|
|
|
|
## What Normally Happens to State Inside a Callback?
|
|
|
|
In our first callback function we didn't define any names. To demonstrate what
|
|
typically happens to state inside the scope of a callback let's make a function
|
|
that creates some state.
|
|
|
|
```python
|
|
def quux():
|
|
plugh = "xyzzy"
|
|
return plugh
|
|
```
|
|
|
|
When we execute this function we get the expected result.
|
|
|
|
```python
|
|
>>> quux()
|
|
'xyzzy'
|
|
```
|
|
|
|
After the function is executed we can see that the `plugh` name is not defined.
|
|
|
|
```python
|
|
>>> plugh
|
|
Traceback (most recent call last):
|
|
File "<stdin>", line 1, in <module>
|
|
NameError: name 'plugh' is not defined
|
|
```
|
|
|
|
This is because when the function is finished executing its frame is removed
|
|
from the [call stack][call stack] along with any locally defined variables. By
|
|
itself our callback can't remember anything.
|
|
|
|
## Stateful Callbacks
|
|
|
|
Alright, so we know what callbacks are, we know what state is. How can we
|
|
combine the two to make a callback that retains its state? As we saw above we
|
|
can't rely on any state that we define inside our callback. The trick to making
|
|
a stateful callback is to rely on names bound to an external scope.
|
|
|
|
To motivate creating a stateful callback let's say that we still want to sort
|
|
users like we did above, only now we have 1 Million users. It's going to take
|
|
a while to sort those users, so it would be nice to have a progress report so
|
|
we know something is still happening, maybe once per 10,000 users.
|
|
|
|
### Using Functions
|
|
|
|
To use names bound to an external scope with a plain ol' function as our
|
|
callback we'll need to take advantage of [closures][closures] (which could be
|
|
an entirely separate post). Here's a function that allows us to use our
|
|
original `by_age` and `by_name` sorters while still giving us progress.
|
|
|
|
```python
|
|
def sort_reporter(func):
|
|
state = dict(count=0) # We can't just call this 'count'...
|
|
|
|
def _sort_reporter(user):
|
|
state['count'] += 1 # Because we'd get an UnboundLocalError here.
|
|
if state['count'] % 10000 == 0:
|
|
print("Sorted {count} users.".format(count=state['count']))
|
|
return func(user)
|
|
|
|
return _sort_reporter
|
|
```
|
|
|
|
We can use it like so.
|
|
|
|
```python
|
|
>>> sorted_users = sorted(users, key=sort_reporter(by_name))
|
|
Sorted 10000 users.
|
|
Sorted 20000 users.
|
|
# Lots more of this...
|
|
```
|
|
|
|
How does it work? The key is the `state` dictionary. It lets us keep a mutable
|
|
reference to a name defined outside the scope of the actual reporter function,
|
|
`_sort_reporter`. As the `sorted` built-in is processing our `users` each new
|
|
call to `_sort_reporter` still gets to refer to the original `state`.
|
|
|
|
<small><b>Note</b>: We could avoid having a `state` dictionary by using Python
|
|
3's `nonlocal` keyword, but then I'd miss an opportunity in the [bonus](#bonus)
|
|
section.</small>
|
|
|
|
### Using Classes
|
|
|
|
If the functional approach doesn't suit you we can also tackle this problem
|
|
from an object-oriented angle. Python lets classes define a
|
|
[`__call__` method][__call__] which makes them callable. This isn't strictly
|
|
necessary for an OOP approach, but when we're making callbacks it's nice to be
|
|
able to treat our instances as functions.
|
|
|
|
```python
|
|
class SortReporter:
|
|
def __init__(self, func):
|
|
self.func = func
|
|
self.count = 0
|
|
|
|
def __call__(self, user):
|
|
self.count += 1
|
|
if self.count % 10000 == 0:
|
|
print("Sorted {count} users.".format(count=self.count))
|
|
return self.func(user)
|
|
```
|
|
|
|
Just as easy to use as our functional option.
|
|
|
|
```python
|
|
>>> sorted_users = sorted(users, key=SortReporter(by_age))
|
|
Sorted 10000 users.
|
|
Sorted 20000 users.
|
|
# One eternity later...
|
|
```
|
|
|
|
Conceptually, this works for much the same reason that the functional approach
|
|
does. The `SortReporter` instance and all its associated state lives on because
|
|
the `sorted` built-in is carrying around a reference to it and it just pretends
|
|
to be a plain ol' function whenever `sorted` needs it to be one.
|
|
|
|
### Which Should I Use?
|
|
|
|
Neither approach is any more or less valid than the other. For this particular
|
|
example there isn't much more code or complexity either way. I generally regard
|
|
functions as being simpler than classes, so I prefer those when possible, but
|
|
classes also provide good structure for more complex callbacks. Try them both!
|
|
|
|
#### Bonus
|
|
|
|
As <s>homework</s> a bonus, try instantiating a `SortReporter` and examining
|
|
its `__dict__` attribute. Meditate on what you find there and how it relates
|
|
to the `state` dictionary in the functional approach.
|
|
|
|
If you get really bold and want to try for extra credit assign the return value
|
|
of the `sort_reporter` function to some variable and examine its `__closure__`
|
|
attribute. This may help you explain why the `state` dictionary doesn't
|
|
disappear after the `sort_reporter` function is called.
|
|
|
|
[call stack]: https://en.wikipedia.org/wiki/Call_stack
|
|
[closures]: https://en.wikipedia.org/wiki/Closure_(computer_programming)
|
|
[__call__]: https://docs.python.org/3/reference/datamodel.html#object.__call__
|