mirror of
https://github.com/jbranchaud/til
synced 2026-07-02 23:58:25 +00:00
77 lines
2.2 KiB
Markdown
77 lines
2.2 KiB
Markdown
# Argument Defaults Are Evaluated When Function Is Defined
|
|
|
|
When you define a function with any arguments that have default values, those
|
|
default values are evaluated and stored at the time that the function is defined
|
|
(i.e. when it is evaluated by the interpreter). This might feel counter
|
|
intuitive if you are coming from another language, like Ruby, where these kinds
|
|
of defaults are evaluated at call time. This is unremarkable for scalar values
|
|
like `4` or `"fallback"`. It's much more interesting when your defaults are
|
|
function calls.
|
|
|
|
What if our default is something like `datetime.now()`?
|
|
|
|
Here I've defined a `Timer` class that has a `start` and `stop` method. The
|
|
`stop` method can be called with a specific `datetime` value otherwise it falls
|
|
back to `datetime.now()` -- but when is _now_?
|
|
|
|
```python
|
|
from datetime import datetime, timezone
|
|
import time
|
|
|
|
|
|
class Timer:
|
|
def __init__(self):
|
|
self._start = None
|
|
self._stop = None
|
|
|
|
def start(self):
|
|
self._start = datetime.now(timezone.utc)
|
|
self._stop = None
|
|
|
|
def stop(self, at=datetime.now(timezone.utc)):
|
|
print(f"now: {datetime.now(timezone.utc)}")
|
|
print(f" at: {at}")
|
|
self._stop = at
|
|
|
|
elapsed = self._stop - self._start
|
|
return elapsed
|
|
```
|
|
|
|
Here I instantiate a timer, call `start`, sleep for 5 seconds, and then call
|
|
`stop`.
|
|
|
|
```python
|
|
timer = Timer()
|
|
timer.start()
|
|
|
|
time.sleep(5)
|
|
|
|
print(f"Elapsed: {timer.stop()}")
|
|
```
|
|
|
|
Here is what gets printed to `stdout`:
|
|
|
|
```
|
|
now: 2026-05-22 00:45:05.654878+00:00
|
|
at: 2026-05-22 00:45:00.649699+00:00
|
|
Elapsed: -1 day, 23:59:59.999875
|
|
```
|
|
|
|
Notice that the actual _now_ (when the `stop` method is running) is about 5
|
|
seconds after the value of `at`. That is because `at`, which takes on the
|
|
default argument value, is `datetime.now()` as evaluated at the time the
|
|
function is interpreted. It is for that same reason that `self._stop` ends up
|
|
being just a hair earlier than the call to `start` which sets `self._start`.
|
|
Which explains why the _elapsed_ time is a negative value.
|
|
|
|
To avoid this awkwardness all together, set the default as `None` and then
|
|
override `None` at the start of the function:
|
|
|
|
```python
|
|
def stop(self, at = None):
|
|
if at == None:
|
|
at = datetime.now(timezone.utc)
|
|
|
|
# ...
|
|
```
|