6  Functions

Functions encapsulate pieces of logic that we want to use repeatedly. Also, this encapsulation makes testing our code much easier which is rather useful.

6.1 Basic syntax

def identity(a, b):  # only positional arguments
    return a, b
identity(1,2)
(1, 2)
def identity_with_default(a, b=1):  # b is a so called "keyword-argument"
    return a, b
identity_with_default("hello") # argument b is optional, if not passed, the default is used
('hello', 1)
Important

Python functions always return a value, even without a return statement, in which case the return value will be None.

def implicit_return():
    pass # do literally nothing
out = implicit_return()
out, type(out)
(None, NoneType)

This three definitions are equivalent:

def g():
    print("hello")
    return None
    
def h():
    print("hello")
    return
    
def f():
    print("hello")
    # The return statement is implicitly here    

6.2 Arbitrary arguments: *args & **kwargs

We can write a function with arbitrary arguments. For that, we use the syntax *args. The args will be put into a tuple:

def print_args(*args):
    print(type(args))
    for arg in args:
        print(arg)
print_args(1,2)
<class 'tuple'>
1
2
print_args(1, 2, 3, 4)  # we can pass as many as we want!
<class 'tuple'>
1
2
3
4

This logic extends to key-word arguments. We use the syntax **kwargs for that. Since key-words are pairs, they are put into a dictionary (instead of a tuple):

def print_kwargs(**kwargs):  # only positional arguments
    for k, v in kwargs.items(): # it's just a dict!
        print(k, "->", v)
print_kwargs(street="martinistraße", number="52")
street -> martinistraße
number -> 52
print_kwargs(street="martinistraße", number="52", coolness="very-high")
street -> martinistraße
number -> 52
coolness -> very-high

We can combine both *args and **kwargs. This function will take any arguments we pass:

def general(*args, **kwargs):
    print(args)
    print(kwargs)
general(1, 2, first="hello", second="world")
(1, 2)
{'first': 'hello', 'second': 'world'}

6.3 Functions are values

We can assign functions to variables and pass them around, like any other object (people call this to have “functions as first-class citizen”).

def printer(func):
    out = func() # call whatever function we pass
    print(out) # print the output
def greeting():
    return "hello from greeting func"
printer(greeting)
hello from greeting func
f = greeting  # Notice we are not calling it with ()
f
<function __main__.greeting()>
printer(f)
hello from greeting func

6.4 Anonymous functions

There is a shorthand to define functions with this syntax:

lambda [optional-args]: [return-values]
lambda: "say hello"
<function __main__.<lambda>()>
printer(lambda: "hello course")
hello course

It can also take arguments:

f = lambda x: x+1
f(1)
2

A typical use case of anonymous functions:

names = ["anna", "lui", "marco", "ramiro", "tim"]
sorted(names, key=lambda name: name[-1])
['anna', 'lui', 'tim', 'marco', 'ramiro']
sorted(names, key=lambda name: len(name))
['lui', 'tim', 'anna', 'marco', 'ramiro']
ages = [("anna", 93), ("lui", 19), ("marco", 11), ("ramiro", 83)]
sorted(ages, key=lambda name_age: name_age[1])  # name_age is a tuple
[('marco', 11), ('lui', 19), ('ramiro', 83), ('anna', 93)]

6.5 Early return

Python functions have so-called early return, which means a function will exit as soon as it hits the first return statement, for example:

def early():
    if 1 > 0:
        return "first condition"
    if 2 > 0:  # This code will never be evaluated
        return "second condition"
early()
'first condition'

This can help us to simplify code, for example, this two definitions are equivalent:

def f(x):
    if x == 1:
        return "it's 1"
    elif x == 2:
        return "it's 2"
    elif x == 3:
        return "it's 3"
    else:
        return "not 1,2,3"

def ff(x):
    if x == 1:
        return "it's 1"
    if x == 2:
        return "it's 2"
    if x == 3:
        return "it's 3"
    return "not 1,2,3"
f(2), f(4)
("it's 2", 'not 1,2,3')
ff(2), ff(4)
("it's 2", 'not 1,2,3')

6.6 Keyword-only arguments

There’s a way to force the arguments of a function to be keyword-only, which can be useful to avoid mistakes and kindly nudge the users of our code (yes, you yourself too!) to pass the arguments to a function explicitly.

Everything coming after * must be key-word:

def func(a, b, *, c, d):
    print(a, b, c, d)

This will not work (pay attention to the error message):

func("hi", "there")
TypeError: func() missing 2 required keyword-only arguments: 'c' and 'd'

Neither will this:

func("hi", "there", "dear", "students") 
TypeError: func() takes 2 positional arguments but 4 were given
func("hi", "there", "dear", d="students") 
TypeError: func() takes 2 positional arguments but 3 positional arguments (and 1 keyword-only argument) were given

Only passing c and d explicitly will do:

func("hi", "there", c="dear", d="students") 
hi there dear students

6.7 Exercises

  1. Write a function called intro that takes two positional arguments, name and age, and prints a sentence introducing a person. The function should return nothing.
  2. Repeat it, but adding an optional argument, city. The function should now return the introducing string (consider handling the city)
  3. Write a function called sort_dict_by_value that takes a dictionary and returns a dictionary sorted by value.
  4. Write a function that takes an arbitrary number of key-word only arguments representing pairs (name, age) and returns a list of tuples sorted by age. For example:
# For this pairs
your_function(lua=32, mark=12)
# Should output:
[("mark", 12), ("lua", 32)]

The very same function should be able to deal with other number of arguments, eg:

your_function(lua=32, mark=12, anna=42)
# Should output:
[("mark", 12), ("lua", 32), ("anna", 42)]

Hint: Try to use the function sort_dict that you wrote in the previous point.

  1. Write a function that takes 2 arguments items (a container of elements,no matter what they are) and sorting_func (a function that will somehow sort the elements of items) and returns the result of the sorting function applied to items. For example, your function should behave like this:
your_function([3, 2, 1], sorted)
# Should output:
[1, 2, 3]

your_function({"a": 2, "b": 1}, sort_dict)
# Should output:
{"b": 1, "a": 2}