# Before we proceed to look at closures, there are a few items that # we should catch up on with regard to ‘regular’ functions. First, # it's been mentioned earlier that ‹def› is really just a statement. # There is more to that: it is a hidden «binding» (assignment) – # saying ‹def foo(): …› is equivalent to ‹foo = …›, except there is # no anonymous ‘function literal’ (‹lambda› comes close, but is # syntactically too different to be a realistic equivalent). # However, ‹mypy› really does not like rebinding names of functions # (it treats functions specially, much more so than Python itself), # so we won't get away with a demo of that. But you can try the # following without ‹mypy› and it will work fine: # # def foo(): return 1 # python # def foo(): return 2 # assert foo() == 2 # Anyway, on to actually useful things. First, let's look at # «implicit parameters»: def power_sum( values: list[ float ], power: float = 1 ) -> float: total = 0 for v in values: total += v ** power return total def test_power_sum() -> None: # demo assert power_sum( [ 1, 2 ] ) == 3 assert power_sum( [ 1, 2 ], 2 ) == 5 # This is basically self-explanatory. When we call the function, we # may either supply the parameter (in which case ‹power› is bound to # the actual value we provided) or not, in which case it is bound to # the «implicit value» (‹1› in this case). # There is a well-known trap associated with implicit parameters. # The problem is that the implicit binding takes place at the time # ‹def› is evaluated (‹def› is just a statement, remember?). # Consider this function with an optional output parameter: def power_list( values: list[ float ], power: float, out: list[ float ] = [] ) -> float: total = 0 for v in values: out.append( v ** power ) return sum( out ) def test_power_list() -> None: # demo assert power_list( [ 1, 2 ], 2 ) == 5 assert power_list( [ 1 ], 1 ) == 6 # Yes, that second ‹assert› is correct. When invoked a second time, # the implicit binding of ‹out› is still the same as it was at the # start, to some cell that was created when the ‹def› executed. And # the second call simply appends more values to that same cell. It # all makes sense, if you think about it. But it is not something # you would intuitively expect, and it is easy to fall into the trap # even if you know about it. # Just to drive the point home, let's consider the following (we # need to pull in ‹Callable› for typing the outer function): from typing import Callable def make_foo() -> Callable[ [], list[ int ] ]: def foo( l: list[ int ] = [] ) -> list[ int ]: l.append( 1 ) return l return foo def test_foo() -> None: # demo # Evaluating the ‹def› again creates a new binding for the # implicit parameter, as expected: assert make_foo()() == [ 1 ] assert make_foo()() == [ 1 ] # If we remember the result of a single ‹def› and call it twice, # we are back where we started (again, as expected): foo = make_foo() assert foo() == [ 1 ] assert foo() == [ 1, 1 ] # Another feature worth mentioning are «keyword arguments». In # Python, with the exception of a couple special cases, all # arguments are ‘keyword’ by default. That is, whether an argument # is used as a keyword argument or a positional argument is up to # the caller to decide. To wit: def test_keyword() -> None: # demo assert power_sum( [ 1, 2 ], power = 4 ) == 17 assert power_sum( values = [ 3, 4 ] ) == 7 # There are some limitations: all positional arguments (in the call) # must precede all keyword arguments – no backfilling is done, so # you cannot skip a positional argument and provide it as a keyword. # If you want to pass an argument using a keyword, you must also do # that for all the subsequent (formal) arguments. Implicit arguments # may be of course left out entirely, but if they are not, they take # effect «after» supplied keyword arguments. # With that out of the way, the main exception from ‘all arguments # are keyword arguments’ are «variadic functions»¹ that take a # ‹tuple› of arguments (as opposed to a ‹dict› of them). Like this # one: def sum_args( *args: int ) -> int: total = 0 for a in args: total += a return total # In the body of the function, ‹args› is a ‹tuple› with an # unspecified number of elements which must all be of the same type, # as far as ‹mypy› is concerned (Python as such doesn't care, # obviously). Of course, that type might be an union, but the body # might involve some ‹isinstance› gymnastics. # Anyway, the function is used as you would expect (of course, since # no names are given to the arguments, they cannot be passed using # keywords): def test_sum_args() -> None: # demo assert sum_args() == 0 assert sum_args( 1 ) == 1 assert sum_args( 1, 2 ) == 3 assert sum_args( 3, 2 ) == 5 # There is another type of variadic function, which «does» permit # (and in fact, «requires» keyword arguments): def make_dict( **kwargs: int ) -> dict[ str, int ]: return kwargs def test_make_dict() -> None: # demo assert make_dict() == {} assert make_dict( foo = 3 ) == { 'foo': 3 } # All of the above can be combined, but the limitations on call # syntax remain in place. In particular: def bar( x: int, *args: int, y: int = 0, **kwargs: int ) -> int: return x + sum( args ) + y + sum( kwargs.values() ) # Note that ‹y› cannot be passed as a non-keyword argument, because # ‹*args› is greedy: it will take up any positional arguments after # ‹x›. On the other hand, if ‹x› is passed as a keyword argument, # ‹args› will be necessarily empty and everything must be passed as # keyword args. def test_bar() -> None: # demo assert bar( 0 ) == 0 assert bar( x = 0 ) == 0 assert bar( 1, 1 ) == 2 assert bar( 1, 1, y = 3 ) == 5 assert bar( 1, 1, y = 3, z = 1 ) == 6 assert bar( x = 1, z = 1 ) == 2 # ¹ The other exception are certain built-in functions, which have # documented parameter names, but those names cannot be used as # keywords in calls. E.g. ‹int()› takes, according to ‹help(int)› # an argument called ‹x›, but you cannot write ‹int( x = 3 )›. You # can, however, say ‹int( '33', base = 5 )›. if __name__ == '__main__': test_power_sum() test_power_list() test_foo() test_keyword() test_sum_args() test_make_dict() test_bar()