Lesson 09 / 16
Function Definition
A default value is evaluated once when the definition is read, not on every call: five calls make one evaluation, a mutable default accumulates five items across five calls, and four of six parameters can be given only one way.
Contents
The previous lesson measured that a loop is a protocol and showed that an exception decides when the turns end. A loop body most often calls a function; it passes that function different values on every turn.
The Programming Fundamentals course’s function lesson built the parameter-versus- argument distinction, the return value, and the call frame; it explained what a function is and reduced argument passing to a single question — which value goes to which parameter. In Python the answer is not one-line: a parameter can be given only by position, only by name, or by both; surplus arguments can be collected; parameters can be bound to a default value. One of these is not about the value but about timing, and that is what this lesson measures: when is a default value computed — on every call, or once?
A Default Is an Expression
def is a statement, and when run it produces a function object. During that
production the body is not run — that lesson was already built — but the
default-value expressions in the signature are run. Their results are computed and
written onto the function object.
The meaning of this is: a default value is part of the definition, not the call. It is not recomputed at call time; the object computed once when the definition is read is rebound on every call. Five calls do not produce five separate defaults; they use the same object five times.
“Once” does not mean once over the program’s lifetime; it means once every time the definition statement runs. A top-level definition runs once while the file is read, so its default is computed once too. A definition written inside another function’s body, however, reruns on every call of the outer function and recomputes its own default. The measured rule does not change — the default is still part of the definition — but how many times the definition runs does.
If the value is an immutable object, this distinction is invisible. A number or a string being the same object on every call has no consequence, because there is no way to change it. The distinction appears once the default is mutable: if the body changes it, the change carries over to the next call.
Sentinel Value
The pattern set against this behavior is making the default None and producing the
real value in the body. None is immutable, so sharing it has no consequence; the
body builds a fresh object on every call. This role is called a sentinel value:
it is not used itself, it only carries the information “no argument was given.”
The sentinel need not be None, but there is a reason it usually is. If None is
also among a parameter’s valid values, it cannot be chosen as the sentinel — then
“not given” and “None was given” cannot be told apart, and a separate object is
made the sentinel instead.
How Many Ways an Argument Can Be Given
The second measure is the number of argument forms. Python recognizes two markers in a signature. A slash announces that the parameters to its left can be given only by positional argument. A star announces that the parameters to its right can be given only by keyword argument. What falls between the two can be given either way.
Two collectors are added to this. A starred parameter collects unmatched positional arguments into a tuple; a double-starred parameter collects unmatched keyword arguments into a dictionary. Together the two are called a variadic argument, and they free the signature from an upper bound on how many arguments it can take.
The order of these pieces in a signature is not free: first the positional-only ones, then the ones giveable either way, then the positional collector, then the keyword-only ones, and last the keyword collector. This order exists to make binding unambiguous. Arguments at the call site are read left to right; positional ones fill the signature’s positional parameters in order, the ones they cannot fill go into the collector, keyword ones match by name, and the ones that do not match fall into the keyword collector. Break this order and the same call could have more than one valid binding, with no way to tell from what was written which one gets chosen.
The same reasoning also requires parameters carrying a default to come after ones that do not — on the positional side. No such constraint applies keyword-only: since matching is by name there, parameters with and without a default can mix freely.
The measurement’s assumptions:
- CF16 — The oracle is the rig itself: the default expression is a function call and logs every evaluation of itself; the measure for “how many times was it evaluated” is this log.
- CF17 — The evaluation count is read at two points: right after the definition statement runs, and after five calls are made. The difference between the two is the evaluation the calls added.
- CF18 — Two functions do the same job and differ only in their default: one an
empty list, the other a
Nonesentinel. The rest of their bodies is identical. - CF19 — Sharing is shown by comparing the object two consecutive calls return
with
is; no identity number is ever printed. The object compared is a list; caching of small immutable values does not enter the measurement. - CF20 — The signature carries six parameters and two collectors; these counts are specific to this signature and do not carry over to another one.
- CF21 — For calls that fail to bind, only the exception’s class is printed, not its message; the message text is not part of the measurement.
- CF22 — Where the default sits is shown by reading the field on the function object that carries defaults; what is read is not a memory fact, it is a field the language itself defines.
- CF23 — The unpacking notation at a call site uses the same markers as the collectors in a signature but works in the reverse direction; the measurement compares the two on the same signature.
Measurement
"""When is a default value evaluated, and how do arguments bind.""" EVALUATIONS = [] def stamp(name): """Records every time it is called; used as a default-value expression.""" EVALUATIONS.append(name) return len(EVALUATIONS) def order(x, d=stamp("default expression")): return d print(f"evaluations right after definition: {len(EVALUATIONS)}") results = [order(i) for i in range(5)] print(f"evaluations after five calls: {len(EVALUATIONS)}") print(f"what five calls returned: {results}") def accumulate(x, bucket=[]): bucket.append(x) return bucket def sentinel(x, bucket=None): if bucket is None: bucket = [] bucket.append(x) return bucket print() print(f"{'call':>6s} {'bucket=[] length':>17s} {'bucket=None length':>19s}") previous_a = previous_b = None for i in range(1, 6): a, b = accumulate(i), sentinel(i) print(f" {i:4d} {len(a):17d} {len(b):19d}") if i > 1: same_a, same_b = a is previous_a, b is previous_b previous_a, previous_b = a, b print(f"is two consecutive calls' bucket the same object: " f"bucket=[] {same_a}, bucket=None {same_b}") print(f"the default sitting on the function object: " f"accumulate {accumulate.__defaults__}, sentinel {sentinel.__defaults__}") print(f"is the returned list the very default sitting on the function: " f"{accumulate(6) is accumulate.__defaults__[0]}") def register(a, b, /, c, d=40, *extra, e, f=60, **rest): """a,b positional-only; e,f keyword-only; extra and rest collect the surplus.""" return {"a": a, "b": b, "c": c, "d": d, "extra": extra, "e": e, "f": f, "rest": rest} CALLS = ( "register(1, 2, 3, e=5)", "register(1, 2, c=3, e=5)", "register(1, 2, 3, 4, 7, 8, e=5, f=9, g=0)", "register(1, 2, 3)", "register(a=1, b=2, c=3, e=5)", ) print() print(f"{'call':<44s} binding") for text in CALLS: try: result = eval(text) bound = (f"a={result['a']} b={result['b']} c={result['c']} d={result['d']} " f"extra={result['extra']} e={result['e']} f={result['f']} rest={result['rest']}") except TypeError as error: bound = type(error).__name__ print(f" {text:<42s} {bound}") POSITIONAL = (1, 2, 3) KEYWORD = {"e": 5, "g": 0} unpacked = register(*POSITIONAL, **KEYWORD) print(f" {'register(*POSITIONAL, **KEYWORD)':<42s} " f"a={unpacked['a']} b={unpacked['b']} c={unpacked['c']} d={unpacked['d']} " f"extra={unpacked['extra']} e={unpacked['e']} f={unpacked['f']} " f"rest={unpacked['rest']}") print() print("parameter positional-giveable keyword-giveable") for name, positional, keyword in (("a", True, False), ("b", True, False), ("c", True, True), ("d", True, True), ("e", False, True), ("f", False, True)): print(f" {name:<9s} {('yes' if positional else 'no'):>17s}" f" {('yes' if keyword else 'no'):>18s}")
evaluations right after definition: 1
evaluations after five calls: 1
what five calls returned: [1, 1, 1, 1, 1]
call bucket=[] length bucket=None length
1 1 1
2 2 1
3 3 1
4 4 1
5 5 1
is two consecutive calls' bucket the same object: bucket=[] True, bucket=None False
the default sitting on the function object: accumulate ([1, 2, 3, 4, 5],), sentinel (None,)
is the returned list the very default sitting on the function: True
call binding
register(1, 2, 3, e=5) a=1 b=2 c=3 d=40 extra=() e=5 f=60 rest={}
register(1, 2, c=3, e=5) a=1 b=2 c=3 d=40 extra=() e=5 f=60 rest={}
register(1, 2, 3, 4, 7, 8, e=5, f=9, g=0) a=1 b=2 c=3 d=4 extra=(7, 8) e=5 f=9 rest={'g': 0}
register(1, 2, 3) TypeError
register(a=1, b=2, c=3, e=5) TypeError
register(*POSITIONAL, **KEYWORD) a=1 b=2 c=3 d=40 extra=() e=5 f=60 rest={'g': 0}
parameter positional-giveable keyword-giveable
a yes no
b yes no
c yes yes
d yes yes
e no yes
f no yes
An Expression Evaluated Once
The first three lines close the timing question. Right after the definition statement runs, before any call is made yet, the evaluation count is 1. The default expression ran while the definition was read.
After five calls the count is still 1. The calls added no evaluation. The third line shows this directly: all five calls return the same value — every one of them sees the single object computed at definition time.
There is no link between how many times a function is called and how many times its default is evaluated. This is why it is wrong to read an expression written in the signature as if it were written in the body: one runs once, the other on every call.
The Shared Bucket
The middle table makes this single evaluation pay its cost. In the left column the default is an empty list; over five calls the length grows 1, 2, 3, 4, 5. Every call finds the list the previous one left behind.
In the right column, the sentinel pattern; the length stays 1 across all five calls. Every call builds its own list and inherits nothing from the one before.
The bottom line states the reason through identity. The object two consecutive calls
return in the bucket=[] form comes out true under the is test: the same
object. In the sentinel pattern, false: separate objects. This is where the
assumption that a function gets “a new list” on every call breaks — the list was
built once at definition time, bound to the function object, and from that moment on
lives as a single object that every call mutates.
The second-to-last line shows where the default sits, and this is the lesson’s
most direct proof. When the field on the function object carrying defaults is read,
it holds [1, 2, 3, 4, 5] — the list five calls accumulated, sitting, after the
calls are done, on the function itself. In the sentinel pattern the same field
holds only None; nothing accumulates there, because the accumulating lists were
built in the body and left behind with the call.
The last line closes the loop: a call’s returned list and the default sitting on the
function object come out true under the is test. The two are a single object.
The sentence “the default is part of the definition” stops being a comment here; it
becomes something readable off the function itself.
The result itself is not a bug, it is the rule’s direct consequence. The Programming Fundamentals course wrote this trap as a warning; the numbers here give the reason for it. For someone who knows the rule, the behavior is not surprising: if the body changes the default, what it changes is part of the definition.
Six Parameters, Two Markers
The bottom two tables measure binding. The first two calls give the same result: c
is given once by position, once by name, and the binding is exactly the same. Because
d, e, and f are not given, d and f fall to their defaults; e has to be
written at the call site regardless, because it has no default and can only be given
by keyword.
The third call fills both collectors. Six positional arguments are given; the first
four satisfy a, b, c, d, and the surplus two go into the extra tuple.
On the keyword side, e and f find their own parameters, and g, which has no
match in the signature, falls into the rest dictionary. Collectors are parameters
that collect an argument that cannot bind, instead of turning it into an error.
The last two calls fail to bind, and both give TypeError. The fourth is missing
e: a call cannot be built without a keyword-only parameter that has no default. The
fifth has the opposite problem — a and b are given by name, but sit left of the
slash and can only be taken by position. Given by name, they are seen not as
satisfying the parameters but as names that would fall into rest, leaving the two
positional parameters empty.
The sixth line shows what the same markers do at the call site. While a
signature collects with the star markers, a call does the exact opposite: it
unpacks a tuple into positional arguments and a dictionary into keyword
arguments. The three-item tuple satisfies a, b, c; e in the dictionary finds
its own parameter, and g falls into the rest dictionary.
That the two directions use the same markers is not a coincidence: collecting and unpacking are each other’s inverse, and passing a function’s received arguments on to another function unchanged means using both side by side. This is the basic pattern for a wrapping function — it collects arguments without knowing the signature and unpacks them into the function it wraps.
The last table summarizes the whole signature. Four of the six parameters can be
given only one way — a and b only by position, e and f only by name. Only
two accept both ways. This is what the markers do: they narrow how many ways a
parameter can be given. The payoff of narrowing is that the call notation does not
change along with the signature — when a parameter kept positional-only is renamed,
no call breaks, and the same holds when a parameter kept keyword-only changes order.
Summary
- A default-value expression is part of the definition, not the call: it is evaluated once while the definition is read, and five calls do not change that count — there is no link between call count and evaluation count.
- This is invisible with an immutable default; with a mutable one, the body changes part of the definition, and five calls accumulate five items in a single list.
- The
None-sentinel pattern builds a fresh object in the body; the bucket of two consecutive calls comes out as separate objects under theistest, and the length stays 1 on every call. - A slash makes the parameters to its left positional-only, a star makes the ones to its right keyword-only; in the measured signature, four of six parameters can be given only one way, two either way.
- Starred and double-starred collectors gather unmatched positional and keyword
arguments; the same markers work in reverse at a call site, unpacking a tuple and
a dictionary into arguments. An argument that cannot be collected and a parameter
left unfilled both produce
TypeError. - The default sits in the function object’s own field: after five calls, the entire
list accumulated in that field can be read, and the returned list comes out as the
same object under the
istest.
Next Step
A function body uses names beyond the ones arriving with arguments: ones built in the body, ones coming from the definition that encloses it, ones sitting at the outermost level of the file, and ones the language itself supplies. The Programming Fundamentals course built this search order as a language-independent concept. In Python the order has four levels, and what decides which level a name is found in is not where it is read, but where it is assigned. The next lesson defines the same name at all four levels at once and measures which one wins — and how adding a single assignment line to a body moves that same name to a different level.
To keep your progress and take notes, Log in
My notes
Log in to take notes.