[Learning Python: From Zero to One]-Function Parameters-(7)

Charles Ma
13 min readSep 4, 2023

--

When defining a function, we determine the names and positions of the parameters, and the interface definition of the function is completed. For the caller of the function, it is enough to know how to pass the correct parameters and what value the function will return. The complex logic inside the function is encapsulated, and the caller does not need to know.

Python’s function definition is very simple, but it is very flexible. In addition to the normally defined mandatory parameters, default parameters, variable parameters and keyword parameters can also be used, so that the interface defined by the function can not only handle complex parameters, but also simplify the code of the caller.

Positional Parameters

Let’s first write a function to calculate x2:

def power(x):
return x * x

For the power(x) function, the parameter x is a positional parameter.

When we call the power function, we must pass in the only parameter x:

>>> power(5)
25
>>> power(15)
225

Now, what if we want to calculate x3? You can define another power3 function, but what if you want to calculate x4, x5…? We cannot define infinitely many functions.

You may have thought of it, you can change power(x) to power(x, n) to calculate xn, just do it:

def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

For this modified power(x, n) function, any power of n can be computed:

>>> power(5, 2)
25
>>> power(5, 3)
125

The modified power(x, n) function has two parameters: x and n, both of which are positional parameters. When calling the function, the two values passed in are assigned to the parameters x and n in order of position.

Default Parameters

There is no problem with the new power(x, n) function definition, but the old calling code failed because we added a parameter, which caused the old code to fail to call normally because of a missing parameter:

>>> power(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'n'

Python’s error message is clear: the call to function power() is missing a positional parameter n.

At this time, the default parameters come in handy. Since we often calculate x2, it is entirely possible to set the default value of the second parameter n to 2:

def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

Thus, when we call power(5), it is equivalent to calling power(5, 2):

>>> power(5)
25
>>> power(5, 2)
25

For other cases where n > 2, n must be explicitly passed in, such as power(5, 3).

As can be seen from the above examples, default parameters can simplify function calls. When setting default parameters, there are a few points to note:

  1. Mandatory parameters come first, default parameters come second, otherwise Python’s interpreter will report an error (think about why default parameters can’t be placed in front of mandatory parameters);

2. how to set the default parameters.

When a function has multiple parameters, put the parameter with a large change in front, and the parameter with a small change in the back. Parameters with small changes can be used as default parameters.

What are the benefits of using default parameters? The biggest advantage is that it can reduce the difficulty of calling functions.

For example, let’s write a function for the registration of first-grade elementary school students, and we need to pass in two parameters: name and gender:

def enroll(name, gender):
print('name:', name)
print('gender:', gender)

In this way, calling the enroll() function only needs to pass in two parameters:

>>> enroll('Sarah', 'F')
name: Sarah
gender: F

What if you want to continue to pass in information such as age, city, etc.? This will greatly increase the complexity of calling the function.

We can set age and city as default parameters:

def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)

In this way, most students do not need to provide age and city when registering, only provide two necessary parameters:

>>> enroll('Sarah', 'F')
name: Sarah
gender: F
age: 6
city: Beijing

Only students who do not match the default parameters need to provide additional information:

enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')

It can be seen that default parameters reduce the difficulty of function calls, and once more complex calls are required, more parameters can be passed to achieve. Whether it is a simple call or a complex call, only one function needs to be defined.

When there are multiple default parameters, when calling, you can provide the default parameters in order, such as calling enroll('Bob', 'M', 7), which means that in addition to the two parameters of name and gender, the last one The parameter is applied to the parameter age, and the city parameter still uses the default value because it is not provided.

Some default parameters can also be provided out of order. When some default parameters are not provided in order, the parameter names need to be written. For example, calling enroll('Adam', 'M', city='Tianjin') means that the city parameter uses the value passed in, and other default parameters continue to use the default value.

The default parameters are very useful, but if used incorrectly, they can also fall into the pit. The default parameter has the biggest pit, the demonstration is as follows:

First define a function, pass in a list, add an END and return:

def add_end(L=[]):
L.append('END')
return L

The result seems to be fine when you call it normally:

>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']

When you call it with default arguments, the result is also correct initially:

>>> add_end()
['END']

However, when add_end() is called again, the result is wrong:

>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

Many beginners are confused, the default parameter is [], but the function seems to “remember” the list after adding 'END' last time.

The reason is explained as follows:

When the Python function is defined, the value of the default parameter L is calculated, that is, [], because the default parameter L is also a variable, which points to the object [], every time the function is called, if the content of L is changed, then The next time it is called, the content of the default parameter will change, and it will no longer be [] when the function is defined.

One thing to keep in mind when defining default parameters: default parameters must point to immutable objects!

To modify the above example, we can use the None immutable object to achieve:

def add_end(L=None):
if L is None:
L = []
L.append('END')
return L

Now, no matter how many times you call it, there will be no problem:

>>> add_end()
['END']
>>> add_end()
['END']

Why design immutable objects like str and None? Because once the immutable object is created, the data inside the object cannot be modified, which reduces errors caused by modifying the data. In addition, because the object does not change, it does not need to be locked while reading the object in a multi-tasking environment, and there is no problem with reading at the same time. When we write a program, if we can design an invariant object, then try to design it as an invariant object.

Variable Parameters

In Python functions, variable parameters can also be defined. As the name implies, variable parameters mean that the number of parameters passed in is variable, and can be 1, 2 to any number, or 0.

Let’s take a math problem as an example, given a set of numbers a, b, c…, please calculate a2 + b2 + c2 + ….

To define this function, we must determine the input parameters. Since the number of parameters is uncertain, we first think that a, b, c… can be passed in as a list or tuple. In this way, the function can be defined as follows:

def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

But when calling, you need to assemble a list or tuple first:

>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84

If you use variable parameters, the way to call the function can be simplified as follows:

>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84

So we change the parameters of the function to variadic:

def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

Compared with defining a variable parameter and defining a list or tuple parameter, only a * sign is added in front of the parameter. Inside the function, the parameter numbers receives a tuple, so the function code is completely unchanged. However, when calling this function, any number of parameters can be passed in, including 0 parameters:

>>> calc(1, 2)
5
>>> calc()
0

What if you already have a list or tuple and want to call it with a variable parameter? It can be done like this:

>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14

This way of writing is of course feasible, but the problem is that it is too cumbersome, so Python allows you to add an * in front of the list or tuple, and pass the elements of the list or tuple into variable parameters:

>>> nums = [1, 2, 3]
>>> calc(*nums)
14

*nums means that all elements of the nums list are passed in as variable parameters. This way of writing is quite useful and very common.

Keyword Parameters

Variable parameters allow you to pass in 0 or any number of parameters, and these variable parameters are automatically assembled into a tuple when the function is called. The keyword parameter allows you to pass in 0 or any parameters with parameter names, and these keyword parameters are automatically assembled into a dict inside the function. See example:

def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

In addition to the mandatory parameters name and age, the function person also accepts the keyword parameter kw. When calling this function, you can only pass in the required parameters:

>>> person('Michael', 30)
name: Michael age: 30 other: {}

You can also pass in any number of keyword arguments:

>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

What are keyword arguments for? It can extend the functionality of the function. For example, in the person function, we are guaranteed to receive the two parameters name and age, but if the caller is willing to provide more parameters, we can also receive them. Imagine that you are doing a user registration function. Except for the user name and age, the other items are optional. Using keyword parameters to define this function can meet the registration requirements.

Similar to variable parameters, you can also assemble a dict first, and then convert the dict into keyword parameters and pass it in:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

Of course, the above complex call can be written in a simplified way:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra means to pass all the key-values of the extra dict to the **kw parameter of the function with keyword parameters, and kw will get a dict. Note that the dict obtained by kw is a copy of extra, and the changes to kw are not It will affect the extra outside the function.

Named Keyword Parameters

For keyword arguments, the caller of the function can pass in any unrestricted keyword arguments. As for what is passed in, you need to pass the kw check inside the function.

Still taking the person() function as an example, we want to check whether there are city and job parameters:

def person(name, age, **kw):
if 'city' in kw:
pass
if 'job' in kw:
pass
print('name:', name, 'age:', age, 'other:', kw)

But callers can still pass in unlimited keyword arguments:

>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

If you want to limit the names of keyword parameters, you can use named keyword parameters, for example, only accept city and job as keyword parameters. Functions defined this way are as follows:

def person(name, age, *, city, job):
print(name, age, city, job)

Unlike keyword arguments **kw, named keyword arguments require a special delimiter *, and arguments after * are treated as named keyword arguments.

The calling method is as follows:

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

If there is already a variable parameter in the function definition, the following named keyword parameters do not need a special separator *:

def person(name, age, *args, city, job):
print(name, age, args, city, job)

Named keyword arguments must be passed the argument name, unlike positional arguments. If no parameter name is passed in, the call will report an error:

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'

Due to the lack of parameter names city and job when calling, the Python interpreter regards the first two parameters as positional parameters, and the last two parameters are passed to *args, but the lack of named keyword parameters results in an error.

Named keyword arguments can have default values, simplifying calls:

def person(name, age, *, city='Beijing', job):
print(name, age, city, job)

Since the named keyword parameter city has a default value, the city parameter may not be passed in when calling:

>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

When using named keyword parameters, pay special attention. If there are no variable parameters, you must add a * as a special separator. If the * is missing, the Python interpreter will not recognize positional and named keyword arguments:

def person(name, age, city, job):
# Missing *, city and job are considered positional arguments
pass

Parameters Combination

To define a function in Python, you can use mandatory parameters, default parameters, variable parameters, keyword parameters and named keyword parameters, all of which can be used in combination. Note, however, that the order of parameter definitions must be: required parameters, default parameters, variable parameters, named keyword parameters, and keyword parameters.

For example, define a function that contains the above-mentioned several parameters:

def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

When the function is called, the Python interpreter automatically passes in the corresponding parameters according to the parameter position and parameter name.

>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

The most amazing thing is that with a tuple and dict, you can also call the above function:

>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

Therefore, for any function, it can be called in a form similar to func(*args, **kw), no matter how its parameters are defined.

While you can combine up to 5 kinds of parameters, don’t use too many combinations at the same time, or the function interface will be poorly understood.

Recursive Function

Inside functions, other functions can be called. A function is recursive if it calls itself internally.

For example, let’s calculate the factorial n! = 1 x 2 x 3 x … x n, expressed by the function fact(n), we can see that:

fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n

Therefore, fact(n) can be expressed as n x fact(n-1), and only when n=1 requires special handling.
So fact(n) written recursively is:

def fact(n):
if n==1:
return 1
return n * fact(n - 1)

The above is a recursive function. You can try:

>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

If we calculate fact(5), we can see that the calculation process is as follows according to the function definition:

===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

The advantages of recursive functions are simple definition and clear logic. In theory, all recursive functions can be written as loops, but the logic of loops is not as clear as that of recursion.

When using recursive functions, care must be taken to prevent stack overflow. In a computer, a function call is implemented through a data structure called a stack. Whenever a function call is entered, a stack frame will be added to the stack, and every time the function returns, the stack will be decremented by a stack frame. Since the size of the stack is not infinite, too many recursive calls will cause the stack to overflow. You can try fact(1000):

>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison

The way to solve the recursive call stack overflow is through tail recursion optimization. In fact, the effect of tail recursion and loop is the same, so it is also possible to regard loop as a special tail recursive function.

Tail recursion means that when a function returns, it calls itself, and the return statement cannot contain expressions. In this way, the compiler or interpreter can optimize the tail recursion, so that no matter how many times the recursion itself is called, it only occupies one stack frame, and there will be no stack overflow.

The above fact(n) function is not tail recursive because return n * fact(n — 1) introduces a multiplication expression. To change to the tail-recursive method, a little more code is needed, mainly to pass the product of each step into the recursive function:

def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

It can be seen that return fact_iter(num — 1, num * product) only returns the recursive function itself, and num - 1 and num * product will be calculated before the function call, without affecting the function call.

The call of fact_iter(5, 1) corresponding to fact(5) is as follows:

===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120

When tail recursive calls are made, the stack will not grow if optimized, so no matter how many calls are made, the stack will not overflow.

Unfortunately, most programming languages are not optimized for tail recursion, and neither is the Python interpreter. Therefore, even if the above fact(n) function is changed to tail recursion, the stack will overflow.

--

--