A Familiar Program
C++// Euklid's algorithm (with modulo)
int gcd(int x, int y) {
assert(0 < x && 0 < y);
while (y > 0) {
int t = x % y;
x = y;
y = t;
}
return x;
}
...
std::cout << gcd(6, 9); // 3
Python# Euklid's algorithm (with modulo)
def gcd(x, y):
assert 0 < x and 0 < y, \
"Arguments must be positive"
while y > 0:
t = x % y
x = y
y = t
return x
print(gcd(6, 9)) # 3
First Observations
A Python function declaration has the shape
def NAME(X1, ..., XN):
BODY
where NAME
is the function’s name, X1
to XN
are parameter names, and BODY
is the function body. Python functions thus look similar to C++ functions, but with types removed, def
added, and an indented body instead of a code block in curly braces.
Function calls in Python also resemble those you know from C++: R = NAME(E1, ..., EN)
calls function NAME
, passes the results of evaluating expressions E1
to EN
as arguments, and stores the call’s return value in R
.
Forward Declarations (Not Needed)
C++// Forward declaration of is_odd
bool is_odd(unsigned int x);
bool is_even(unsigned int x) {
return x == 0 || is_odd(x - 1);
}
bool is_odd(unsigned int x) {
return x != 0 && is_even(x - 1);
}
Pythondef is_even(x):
assert 0 <= x
return x == 0 or is_odd(x - 1)
def is_odd(x):
assert 0 <= x
return x != 0 and is_even(x - 1)
Consider the above programs, which (very inefficiently) determine whether a positive number is even or odd. The C++ program requires a forward declaration of is_odd
; otherwise, the compiler will complain that is_even
cannot call is_odd
because the latter is not yet known. Technically, this is because the C++ compiler only passes over a program once, from top to bottom, whereas compilers for most other languages pass twice over a program. Python also does the latter, and thus doesn’t require forward declarations.
Function Overloading
In C++ and other statically typed languages, function overloading allows you to implement different functions with the same name, but different arguments. This by itself would result in ambiguity at call-site – which function to call? – but the ambiguity is resolved by matching formal against actual arguments, i.e. by comparing the types of the arguments at call site with the types of the function declaration.
As an example, consider a function that outputs different types of values in different ways – for simplicity, integers and reals with a corresponding output prefix:
C++void show(int i) {
std::cout << "integer value = " << i << '\n';
}
void show(double d) {
std::cout << "real value= " << d << '\n';
}
// Calls to show(int)
show(25); // integer value = 25
show(-12); // integer value = -12
// Calls to show(double)
show(0.46); // real value= 0.46
show(-1.2); // real value= -1.2
Function overloading is not possible in Python, due to the lack of static types. However, function overloading is also not an essential programming language feature (i.e. it does not increase a language’s expressiveness) because it can be “simulated” by having multiple functions with different names, e.g. show_int
and show_double
, combined with checking the type of values/objects at runtime. In Python, the latter is done via isinstance(o, T)
, which is true if and only if object o
is of dynamic type T
.
Python (broken)def show(i):
print("integer value =", i)
# ATTENTION: Since Python does not have static
# types, the next show function simply shadows
# (replaces) the first one!
def show(f):
print("real value =", f)
# All calls go to show(f)
show(25); # real value = 25
show(-12); # real value = -12
show(0.46); # real value = 0.46
show(-1.2); # real value = -1.2
Python (works)def show_int(i):
print("integer value =", i)
def show_float(f):
print("real value =", f)
def show(v):
if (isinstance(v, int)):
show_int(v)
elif (isinstance(v, float)):
show_float(v)
else:
assert false, \
"Unexpected type {}".format(type(v))
# Ultimately calls show_int
show(25); # integer value = 25
show(-12); # integer value = -12
# Ultimately calls show_float
show(0.46); # real value = 0.46
show(-1.2); # real value = -1.2
Operator Overloading
As just seen, function overloading (or its simulation) enables having one function name, e.g. show
, with different implementations and thus behaviours, depending on the type of the arguments. This idea naturally extends to operators – which are essentially functions that we merely call differently: e.g. addition on integers (1 + 2 == 3
) is typically given a different implementation than addition on strings ("H" + "i" == "Hi"
). We will discuss operator overloading in the context of classes, in TODO: some later chapter.
Default Arguments
Assume you are asked to implement a function for converting a decimal integer into its binary representation, i.e. to base 2. A suitable Python function signature would then be to_base_2(n)
for a non-negative integer n
. However, you remember that the algorithm for converting into binary representation also works for other bases, e.g. 3 and 8. Consequently, you implement a more general function to_base(n, b)
where 2 <= b <= 10
is the base to convert to. You’re happy with the result – but your colleagues aren’t: in their code, they nearly always need the binary representation, and they are annoyed that they always have to provide 2
as the argument to your function.
That’s where default arguments come in (also in C++, although we haven’t officially introduced them in the C++ course):
C++std::string to_base(unsigned int n,
unsigned int base = 2) {
assert(2 <= base && base <= 10);
std::string result = "";
while (n != 0) {
result = std::to_string(n % base) + result;
n /= base;
}
return result;
}
...
std::cout << to_base(77) << "\n"; // 1001101
std::cout << to_base(77, 2) << "\n"; // 1001101
std::cout << to_base(77, 5) << "\n"; // 302
Pythondef to_base(n, base = 2):
assert 0 <= n
assert 2 <= base and base <= 10
result = ""
while not n == 0:
result = str(n % base) + result
n = n // base
return result
print(to_base(77)) # 1001101
print(to_base(77, 2)) # 1001101
print(to_base(77, 5)) # 302
Here, if no value is provided for parameter base
when calling to_base
, the default value 2
will be used. Default arguments thus enable implementing functions that are generic/flexible (many parameters), but still convenient to call in many default situations, without having to provide the same, common arguments over and over again.
Default arguments – often combined with keyword arguments, introduced next – are very common in Python. E.g. the print
function specifies four default arguments, one of which (parameter end = "\n"
) determines how to end the output.
Keyword Arguments
Imagine that you are tasked with implementing a function that outputs a sequence of numbers. You brainstorm a bit, and conclude that it would be nice to also output some context information, to be able to specify the character that separates the numbers, and the final character. You thus implement the following Python function (implementation omitted):
Pythondef show(numbers,
context = "",
separator = " ",
suffix = "\n"):
...
# Some usage examples
show([1,2,3]) # 1 2 3
show([1,2,3], "vec: ") # vec: 1 2 3
show([1,2,3], "vec: ", ",") # vec: 1,2,3
show([1,2,3], "", " ", " (end)") # 1 2 3 (end)
You’re happy with the result – until you notice something annoying with the last usage example above: in order to provide the fourth parameter (here, " (end)"
), you need to provide all earlier parameters, even if you don’t want to change their default values (here, ""
and " "
).
Keyword arguments to the rescue:
Python# def show(...) as before
...
show([1,2,3], suffix=" (done)") # 1 2 3 (done)
Python’s keyword arguments feature allows you to use the parameter name at call-site to specify which parameter (think: assign to) you want to provide, regardless of the position of the argument at call site. Consequently, “normal” arguments are called positional arguments, to differentiate them from keyword arguments. In contrast to Python, C++ (currently) only supports positional arguments, but not keyword arguments.
Arbitrary Argument Lists
Let’s take another look at our show
function from above: it is intended to print arbitrarily many numbers, and therefore expects the first argument to be a sequence (technically, an iterable, TODO: see some later chapter) of values. Due to this, we also have to create and pass a sequence (iterable) at call site, e.g. as in the call show([1,2,3])
. In a sense, this requires a step too much: instead of just passing the individual elements, callers need to wrap them in a sequence in order to form a single value (of sequence type) that is then passed to the function.
To avoid the extra step, Python supports arbitrary argument lists, also known as variadic parameters/arguments/functions, as illustrated by the next version of the show
function (for simplicity, with fewer parameters):
Pythondef show(*numbers, suffix = "\n"):
...
show(1) # 1
show(1, 2) # 1 2
show(1, 2, 3) # 1 2 3
show(1, 2, 3, suffix="and so on\n") # 1 2 3 and so on
Such variadic parameters are denoted by a leading *
, as in *numbers
(Python does not use *
for anything pointer-related), and they greedily consume as many arguments as possible. It is thus reasonable to have the variadic parameter last (if any), or to have it followed by keyword arguments only. See the Python documentation for additional details, e.g. argument list unpacking.
Python’s print
function is a typical specimen of such a variadic function. Its signature is:
Pythondef print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
In case you’re interested: C++ provides a couple of language features that come close to Python’s arbitrary argument lists, but they are not as conveniently usable. If you’re interested, start reading on cppreference
(1,
2,
3,
4,
5)
or search on StackOverflow.
Arbitrary Keyword Argument Lists
The concept of arbitrary argument lists can be transferred to keyword arguments as well, which can be handy in (at least) two situations: for functions that can handle arbitrarily many (optional) arguments, and for functions that need to forward many such arguments.
To illustrate the first situation – which often arises in the context of printing/logging/recording arbitrary additional information – let’s consider the following function:
Pythondef myfun(mandatory, **optionals):
# ... actual function implementation omitted ...
print("mandatory =", mandatory)
# Iterate over all keywords (parameter names) in optionals, and access
# the corresponding values with optionals[keyword]
for keyword in optionals:
print(keyword, "=", optionals[keyword]))
myfun("M", name="temp", weight=1.2, is_empty=False)
# Produces the following output:
# mandatory = M
# name = temp
# weight = 1.2
# is_empty = False
To illustrate the second situation, assume that you are given a function magic
that takes many optional arguments, and that you need to call from your own function myfun1
. To allow callers of your function to pass these optional arguments to the underlying function, you can “collect” them in an arbitrary keyword argument list:
Pythondef magic(mandatory, opt1="", opt2=1, opt3=True):
# ... actual function implementation omitted ...
print("mandatory =", mandatory)
print("opt1 =", opt1)
print("opt2 =", opt2)
print("opt3 =", opt3)
def myfun1(arg1, arg2, **optionals):
# ...
# To convert from an arbitrary argument list (optionals) to
# individual arguments (opt1=..., opt2=..., etc.), the list must be
# unpacked (**optionals).
# See also https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists.
magic(arg1 + arg2, **optionals)
# ...
myfun1(2, 2, opt2=22)
# Produces the following output:
# mandatory = 4
# opt1 =
# opt2 = 22
# opt3 = True
An alternative would be to literally copy the optional arguments from the underlying function to the user-exposed function:
Python# def magic(...) as before
def myfun2(arg1, arg2, opt1="", opt2=1, opt3=True):
# ...
magic(arg1 + arg2, opt1, opt2, opt3)
# ...
myfun2(2, 2, opt2=22) # Same output as before (with myfun1)
Caution: myfun1
may seem preferable (no need to copy options), but be aware that myfun2
is actually safer: with myfun1
, users can indeed pass arbitrary arguments to magic
, including too many arguments, and arguments with unexpected types. This may result in errors – raised by magic
, whose call users can’t directly see and may be confused about – or, even worse, unexpected and hard to explain erroneous results.
In case you’re interested: C++ does not support arbitrary keyword argument lists directly, but
container std::map
(or container std::unordered_map
)
can be used to simulate them, at least to some extent. Major differences will be (1) types: C++ maps have a single value type, which complicates storing values of different types; and (2) convenience: must explicitly create a map of arguments, which requires additional code.
Multiple Return Values via Tuples
Recall from TODO: some earlier chapter that Python supports n-ary tuples (pairs, triples, …), which can be unpacked into separate variables. This makes it straightforward to implement functions that return multiple values, as illustrated below. The same is possible in C++, but the resulting code is less concise.
C++#include <algorithm> // for min_element/max_element
#include <tuple> // for tuple, tie
std::tuple<int, int> minmax(
const std::vector<int>& elems)
{
int a =
*std::min_element(elems.begin(), elems.end());
int z =
*std::max_element(elems.begin(), elems.end());
return {a, z};
// More explicit version of previous line:
// return std::tuple<int, int>{a, z};
}
int least, greatest;
std::tie(least, greatest) = minmax(std::vector<int>{7, 40, 11});
std::cout << "least = " << least
<< ", greatest = " << greatest;
Pythondef minmax(elems):
a = min(elems)
z = max(elems)
return (a, z)
least, greatest = minmax([7, 40, 11])
print("least = {}, greatest = {}"
.format(least, greatest))
Generic Functions
Consider a min
function that returns the lesser of two elements: e.g. min(1,2)
returns 1
, and min("Zoo", "Ale")
returns "Ale"
. Implementing such a generic function – one that works for different types of arguments, e.g. integers and strings – is straightforward in Python, since Python is not statically typed. In C++, the same can be achieved with templates, which were only briefly introduced in the C++ course.
C++// A template essentially allows functions to take
// types as arguments
template<typename T>
T min(T x, T y) {
if (x < y) return x;
else return y;
}
int main() {
// The compiler automatically derives the template
// parameter T from the provided arguments
int m1 = min(3, 7); // T is int
std::string m2 = min("Zoo", "Ale"); // T is string
std::cout << "m1 = " << m1
<< ", m2 = " << m2 << '\n';
// Compiler error: T cannot be int and string
// min(3, "Ale");
}
Pythondef min(x, y):
if x < y: return x
else: return y
m1 = min(3, 7)
m2 = min("Zoo", "Ale")
print("m1 = {}, m2 = {}".format(m1, m2))
# Runtime error: comparison x < y in function min
# fails at runtime because integer 3 and string
# "Zeus" cannot be compared.
# min(3, "Zeus")
We will get back to generic functions in the context of classes, see TODO: this later chapter.
Pre- and Postconditions
In the C++ course, we used function pre- and postconditions in two ways: as comments in the code, meant for users as documentation, and with assert statements, to help us uncover bugs more quickly. Both use cases apply to Python as well.