Functions & closures¶
This page covers how you define and call functions in d/Python: the kinds of parameter, default values, *args and **kwargs, lambda, inner functions that remember variables from the function around them (closures), recursion, and passing functions around as values. Arguments are matched to parameters by Python's rules. Each function compiles into a procedure in the compiled program file, .dbc, with a fixed set of argument types and an ordinary call frame, and the compiler reuses that procedure for every call. A closure shares the actual variable with the function that created it, not a copy. Functions are values you can pass around.
Signatures¶
def greet(name, /, greeting="Hello", *, punctuation="!"):
return f"{greeting}, {name}{punctuation}"
print(greet("Ada"), greet("Grace", "Hi"), greet("Guido", punctuation="?"))
def total(*values, **options):
return sum(values) * options.get("scale", 1)
args = (4, 5)
print(total(1, 2, 3), total(1, 2, scale=10), total(*args), total(*(1, 1), scale=2))
Positional-only parameters (before the /), ordinary parameters, and keyword-only parameters (after the *) all work. *values collects extra positional arguments into a tuple whose length the compiler can see at each call, and **options collects extra keyword arguments into a dictionary, in order, whose values all have one type. At a call, * spreads a tuple of known shape or a list written out in the call, and ** spreads a dictionary written out in the call with string keys, in the order they appear; the tests cover both. Argument lists whose shape is only known while the program runs are not yet supported.
Defaults evaluate once¶
The list is created once, when the def line runs, and every call shares it, exactly as in CPython. One limit: default values on methods inside a class must still be simple literals that cannot change, such as numbers and strings.
Closures¶
def make_counter(step=1):
count = 0
def bump():
nonlocal count
count += step
return count
return bump
ones = make_counter()
tens = make_counter(10)
print(ones(), ones(), tens(), ones(), tens())
double = lambda n: n * 2
print(double(21), list(map(double, [1, 2, 3])))
An inner function or a lambda sees the variables of the function around it and shares them rather than copying them: nonlocal lets it assign to them, capture works through several levels of nesting, a closure made in a loop sees the loop variable's final value (Python's late binding), and an inner function can call itself. Each time a def runs it makes a fresh function with its own defaults, which is why ones and tens above count separately. A variable that an inner function uses must be assigned before the inner def runs, with one exception: a function may refer to itself. One limit today: the compiler treats two different functions as two different types, so a list of several lambdas stops the compiler with a mixed-types message, while a list holding the same function several times is fine.
Recursion¶
def fact(n):
if n < 2:
return 1
return n * fact(n - 1)
def even(n):
if n == 0:
return True
return odd(n - 1)
def odd(n):
if n == 0:
return False
return even(n - 1)
print(fact(30), even(10), odd(7))
A function can call itself, and two functions can call each other, up to 128 nested calls; past that, an error is raised that you can catch. Write the base case (the return that does not recurse) before the recursive call: the compiler reads the result type from it. A one-line conditional expression that recurses in one of its branches stops the compiler with exactly that advice.
Functions are values¶
def twice(f, value):
return f(f(value))
def inc(n):
return n + 1
handlers = [inc, inc]
print(twice(inc, 5), handlers[0](1), callable(inc), callable(5))
length = len
print(length("abc"), sorted(["bb", "a"], key=length))
A function can be assigned to a name, passed as an argument, returned, stored in a list and kept in an object's field, as long as the compiler can tell which function it is. You can give a builtin another name, as length = len does above; that works for abs, all, any, ascii, bin, callable, chr, divmod, hex, iter, next, len, max, min, oct, ord, print, repr, reversed, sorted and sum, and the new name calls the builtin it was given. A method taken from an object remembers that object, and __self__ and __func__ show the object and the underlying function. Not yet supported: taking a method of a builtin type as a value (for example counts.get without calling it), descriptors, and a variable whose kind of callable changes while the program runs.
Globals¶
A function can read a variable defined at the top of the file without any declaration, and can assign to it after a global statement. A function you define may have the same name as a builtin, and your definition wins.
What the tests cover¶
Every claim on this page comes from a test program that runs in both d/Python and CPython and must print the same thing. The corpus page lists those programs, with their reviewed output.