mirror of
https://github.com/jbranchaud/til
synced 2026-07-02 23:58:25 +00:00
Add Argument Defaults Are Evaluated When Function Is Defined as a Python TIL
This commit is contained in:
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
|
|||||||
|
|
||||||
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
||||||
|
|
||||||
_1794 TILs and counting..._
|
_1795 TILs and counting..._
|
||||||
|
|
||||||
See some of the other learning resources I work on:
|
See some of the other learning resources I work on:
|
||||||
|
|
||||||
@@ -1056,6 +1056,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Access Instance Variables](python/access-instance-variables.md)
|
- [Access Instance Variables](python/access-instance-variables.md)
|
||||||
- [Access Most Recent Return Value In REPL](python/access-most-recent-return-value-in-repl.md)
|
- [Access Most Recent Return Value In REPL](python/access-most-recent-return-value-in-repl.md)
|
||||||
- [Access Variables Outside Loop Scope](python/access-variables-outside-loop-scope.md)
|
- [Access Variables Outside Loop Scope](python/access-variables-outside-loop-scope.md)
|
||||||
|
- [Argument Defaults Are Evaluated When Function Is Defined](python/argument-defaults-are-evaluated-when-function-is-defined.md)
|
||||||
- [Assert Is Only A Development Check](python/assert-is-only-a-development-check.md)
|
- [Assert Is Only A Development Check](python/assert-is-only-a-development-check.md)
|
||||||
- [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md)
|
- [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md)
|
||||||
- [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md)
|
- [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user