2. Pythonics I
2. Pythonics I
ADVIST
by
Dmitrii Nechaev & Dr. Lothar Richter
28.10.22
Recap
2/136
Recap
3/136
Recap
one
1 my_variable = 1 + 'one'
3/136
Recap: Naming scheme
4/136
Recap: Naming scheme
4/136
Recap: Data Types
5/136
Recap: Data Types
5/136
Recap: Control Flow
6/136
Recap: Control Flow
6/136
Recap: Functions
7/136
Recap: Functions
7/136
Recap: Classes
8/136
Recap: Classes
8/136
Recap: Modules
9/136
Recap: Modules
9/136
Recap: Packages
10/136
Recap: Packages
10/136
Going Deeper
11/136
Today
everything is an object
exceptions
more on assignments
more on functions
more on numbers
more on lists
more on tuples
more on dicts
more on sets
more on strings
more containers via collections
12/136
Everything is an Object
We start by saying that everything is an object in python.
1 def empty_func():
2 '''This is a doc string.'''
3 pass
4 empty_func.__doc__
'empty_func'
1 (1).__add__(2)
13/136
Exceptions
14/136
Exceptions
15/136
Throwing Exceptions
Many things can go wrong. We can try to divide by zero, or try to open a file that doesn’t
exist, or try to pop from an empty stack. In such cases Python program throws an
exception and immediately terminates. We can manually throw an exception via
raise:
1 raise ValueError('We raised this exception ourselves!')
16/136
Catching Exceptions
Oopsie
That said, NEVER use an except without a specific exception you want to catch!
17/136
Catching Exceptions
Python comes with many built‑in exception classes. Right now, let’s catch only
ZeroDivisionError:
1 try:
2 1 / 0
3 except ZeroDivisionError as error:
4 print(error)
5 print(type(error))
division by zero
<class 'ZeroDivisionError'>
18/136
Catching Exceptions
We can have several except clauses:
1 def divide(a, b):
2 try:
3 return a / b
4 except ZeroDivisionError:
5 print(”Can't divide by zero.”)
6 except TypeError:
7 print(”That's not even a number.”)
8
19/136
Catching Exceptions
We can catch different exceptions with one except clause:
1 def divide(a, b):
2 try:
3 return a / b
4 except (
5 ZeroDivisionError, TypeError
6 ) as error:
7 print('Seriously?', error, type(error))
8
9 divide(10, 0)
20/136
Catching Exceptions
We can also specify a block of code that should only be run when no exceptions have
occurred:
1 def divide(a, b):
2 result = 0
3 try:
4 result = a / b
5 except ZeroDivisionError:
6 print('Seriously?')
7 else:
8 print('Yaaaaay!')
9 return result
10
11 divide(10, 2)
Yaaaaay!
5.0
21/136
Catching Exceptions
Why use else instead of putting additional code into the try clause? Because that
improves readability (we need to investigate fewer lines of code wrapped in the try
clause) and prevents accidentally catching an exception that is caused by the additional
code.
22/136
Catching Exceptions
Finally, we can add a block of code that will always be executed as the last task before
the try statement completes via the finally clause.
23/136
Catching Exceptions
1 def sqd(a, b): 1 def sqd_with_finally(a, b):
2 result = 0 2 result = 0
3 try: 3 try:
4 result = a / b 4 result = a / b
5 except ZeroDivisionError as error: 5 except ZeroDivisionError as error:
6 print(error) 6 print(error)
7 else: 7 else:
8 result **= 2 8 result **= 2
9 return result 9 return result
10 print('I always get executed.') 10 finally:
11 print('I always get executed.')
1 sqd(10, 2)
1 sqd_with_finally(10, 2)
25.0
1 sqd(10, '2') I always get executed.
24/136
Exceptions: Overview
25/136
Assignments
26/136
Multiple Assignments
We can assign several values at once:
1 x, y = 10, 20
2 x, y
(10, 20)
We can swap values without using a temporary variable:
1 x, y = y, x
2 x, y
(20, 10)
27/136
Multiple Assignments
We can use multiple assignments with sequences:
1 my_string = 'Hello, world!'
2 a, b, c, d, e, f, g, h, i, j, k, l, m = my_string
3 a
'H'
1 my_range = range(1, 6)
2 a, b, c, d, e = my_range
3 b
28/136
Multiple Assignments
We can use multiple assignments with sequences:
1 my_list = [10, 20, 30, 40, 50]
2 a, b, c, d, e = my_list
3 c
30
1 my_tuple = (100, 200, 300, 400, 500)
2 a, b, c, d, e = my_tuple
3 d
400
29/136
Multiple Assignments
We can use multiple assignments with sets and dicts too:
1 my_dict = {'v': 11, 'w': 12, 'x': 13, 'y': 14, 'z': 15}
2 a, b, c, d, e = my_dict
3 e
'z'
1 my_set = {5, 4, 3, 2, 1}
2 a, b, c, d, e = my_set
3 a, b, c, d, e
(1, 2, 3, 4, 5)
30/136
Multiple Assignments
31/136
Multiple Assignments
[1, 2, 3, 4, 5]
1 a, b, c, _, _ = my_list
2 c
32/136
Multiple Assignments
1
1 b
[2, 3, 4, 5]
33/136
Augmented Assignments
1 x = 1 1 x **= 4
2 x += 1 2 x
3 x 1296
2 1 x //= 6
1 x *= 3 2 x
2 x 216
6
34/136
Augmented assignments
Of course, we can use augmented assignments with more than just numbers:
1 my_set = {1, 2, 3, 4, 5, 6, 7}
2 my_set &= {4, 5, 6, 7, 8, 9, 10}
3 my_set
{4, 5, 6, 7}
1 my_dict = {'a': 1, 'b': 2}
2 my_dict |= {'a': 3, 'c': 4}
3 my_dict
35/136
Ternary Operator
Very often we want to assign a value from a specific set of options to a variable A based
on the value of a variable B. One way to do it is simply by using branching:
1 raining = True
2 if raining:
3 mushroom_growth = 'Good'
4 else:
5 mushroom_growth = 'Bad'
6 mushroom_growth
'Good'
36/136
Ternary Operator
'Good'
37/136
Assignments: Overview
38/136
Functions
39/136
More on Functions
We’ve already seen that functions can accept multiple arguments and can have a return
value:
1 def my_pow(x, power):
2 return x ** power
3
4 cube_of_four = my_pow(4, 3)
5 cube_of_four
64
Now, we will cover default parameter values, keyword arguments, and arbitrary number
of arguments.
40/136
Default Parameter Values
100
If we omit the argument for a parameter with a default value as we invoke the function,
the default value is automatically used.
41/136
Default Parameter Values
Keep in mind that parameters with default values have to appear in the parameter list
after the parameters that don’t have defaults:
1 def my_pow(x=10, power):
2 return x ** power
3 my_pow(2)
42/136
Keyword Arguments
Keyword arguments allow us to pass arguments in any order when calling a function:
1 def my_pow(x, power=2):
2 return x ** power
3 my_pow(power=3, x=10)
1000
43/136
Keyword Arguments
44/136
Keyword Arguments
45/136
Keyword Arguments
'2021.12.31'
46/136
Arbitrary # of Arguments
The *‑operator allows us to create functions that accept arbitrary number of arguments:
1 def average(*args):
2 return sum(args) / len(args)
3
4 average(4, 5, 6, 7, 8)
6.0
This function has only one parameter ‑ args (this name is used by convention). Its value
is a tuple with positional arguments.
47/136
Mixing It Up
We can combine the *‑operator with keyword arguments:
1 def sum_of_powers(*args, power=1):
2 return sum([x ** power for x in args])
3
4 sum_of_powers(1, 2, 3, 4, power=2)
30
1 sum_of_powers(5, 10)
15
Again, the keyword arguments have to follow the positional arguments.
48/136
Unpacking Arguments
6 my_date(*my_values)
'2021.12.31'
49/136
Unpacking Keyword Arguments
'2021.12.31'
50/136
Functions: Overview
51/136
Functions are Objects
Functions are objects, and we can pass them into other functions as arguments. When
would we want to use that? Consider the following examples.
52/136
Functions as Objects
We have a list of strings and want to sort them alphabetically. The sorted function
allows us to do exactly that (keep in mind that it doesn’t sort in place, it returns a new
sorted list):
1 strings = ['Bob', 'Charles', 'Alice']
2 sorted(strings)
53/136
Functions as Objects
We have a list of strings and want to sort them by length. The sorted function accepts
a keyword argument key=func, where func is a function of one argument that
creates a comparison key for sorting:
1 strings = ['Bob', 'Charles', 'Alice']
2 sorted(strings, key=len)
54/136
Functions as Objects
We have a list of strings and want to sort them by their second character. We need a
function that returns a second character of a given string to use as a key:
1 strings = ['Bob', 'Charles', 'Alice']
2
3 def second_char(string):
4 return string[1]
5
6 sorted(strings, key=second_char)
55/136
Lambdas
Lambdas allow us to create a function inline (in place) where it’s needed:
1 sorted(strings, key=lambda x: x[1])
56/136
Numbers
57/136
More on Numbers
We have mentioned integers and floating point values during the first lecture. We have
also looked into comparisons and basic arithmetic operations. Let’s cover additional
numeric data types and additional mathematical operations.
58/136
Complex Numbers
59/136
Decimals
We have mentioned that floats have limited precision:
1 1.10 + 2.20
3.3000000000000003
Decimals, on the other hand, provide fast exact arithmetic:
1 from decimal import getcontext, Decimal
2 getcontext().prec = 7
3 dec_1 = Decimal(1.10)
4 dec_2 = Decimal(2.20)
5 print(dec_1 + dec_2)
3.300000
60/136
Decimals
18.9
Refer to the official documentation for more!
61/136
Fractions
The fractions module provides support for rational number arithmetic. We can create
fractions from integers, floats, decimals, and strings:
1 from fractions import Fraction
2 Fraction(1, 2)
Fraction(1, 2)
1 Fraction(1.5)
Fraction(3, 2)
1 Fraction('3/7')
Fraction(3, 7)
62/136
math module
The math module provides additional mathematical functions:
1 import math
2 math.ceil(5.6)
6
1 math.floor(5.6)
5
1 math.sqrt(81)
9.0
63/136
math module
Would you like to do some combinatorics?
1 math.factorial(6)
720
1 math.comb(10, 3)
120
1 math.perm(10, 3)
720
64/136
math module
-1.0
1 math.degrees(math.pi)
180.0
65/136
statistics module
The statistics module provides functions for calculating mathematical statistics of
numeric data:
1 import statistics
2 numbers = [5, 7, 5, 11, 17]
3 statistics.mean(numbers)
9
1 statistics.mode(numbers)
5
1 statistics.median(numbers)
66/136
random module
The random module implements pseudo‑random number generators:
1 import random
2 random.seed(727)
3 random.randint(1, 5)
5
1 random.randrange(1, 9, 2)
3
1 random.choice('Hello, world!')
'o'
67/136
Numbers: Overview
complex numbers
decimals
fractions
math module
statistics module
random module
68/136
Lists
69/136
More on Lists
We have already mentioned creating lists, concatenating them, and using the append
and extend methods:
1 list_1 = [1, 2]
2 list_2 = [3, 4]
3 list_3 = [5, 6]
4 big_list = list_1 + list_2
5 big_list.extend(list_3)
6 big_list.append(7)
7 big_list
[1, 2, 3, 4, 5, 6, 7]
Let us take a closer look at the list methods now.
70/136
list.clear
We can remove all items from a list via the clear method:
1 big_list
[1, 2, 3, 4, 5, 6, 7]
1 big_list.clear()
2 big_list
[]
71/136
list.copy
The copy method allows us to create a copy of a list:
1 list_1 = [1, 2, 3]
2 list_2 = list_1.copy()
3 list_1[2] += 1
4 list_1
[1, 2, 4]
1 list_2
[1, 2, 3]
72/136
list.copy
However, it creates a shallow copy, so be carefult:
1 list_of_mutables_1 = [
2 [1, 2],
3 [3, 4],
4 [5, 6]
5 ]
6 list_of_mutables_2 = list_of_mutables_1.copy()
7 list_of_mutables_1[1][0] *= 10
8 list_of_mutables_2
73/136
Shallow Copy - Immutables
Let’s take a closer look at the two previous examples:
74/136
Shallow Copy - Immutables
75/136
Shallow Copy - Immutables
76/136
Shallow Copy - Mutables
77/136
Shallow Copy - Mutables
78/136
Shallow Copy - Mutables
79/136
list.count
To count the number of occurences of a specific item in a list, use the count method:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
1 my_list.count('a') 1 my_list.count('c')
3 1
1 my_list.count('b') 1 my_list.count('d')
2 0
80/136
list.index
We can find the first position of a specific item via the index method:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
2 my_list.index('c')
2
1 my_list.index('a')
0
1 my_list.index('d')
81/136
list.insert
We have already added items to a list’s end with the append method. The insert
method allows us to add an item before an arbitrary position:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
2 my_list.insert(0, 'z')
3 my_list
82/136
list.pop
We can remove an item at an arbitrary position using the pop method:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
2 my_list.pop(2)
3 my_list
83/136
list.pop
84/136
list.pop
85/136
list.remove
If we want to remove specific values (instead of values at specific positions), we need to
use remove:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
2 my_list.remove('a')
3 my_list
86/136
list.reverse
87/136
list.sort
88/136
Reversing and Sorting not in Place
What if we want to keep the original data intact? We need to use the following functions:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
1 list(reversed(my_list)) 1 sorted(my_list)
['a', 'a', 'b', 'c', 'b', 'a'] ['a', 'a', 'a', 'b', 'b', 'c']
1 my_list 1 my_list
['a', 'b', 'c', 'b', 'a', 'a'] ['a', 'b', 'c', 'b', 'a', 'a']
89/136
Slicing Lists
We can access several items at once via slicing:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
2 my_list[0:3]
['b', 'a']
The start value is set to 3, the stop value is set to -1, and we get all the items from index
3 to the penultimate one.
90/136
Slicing Lists
We can omit the start or the stop value when we slice from the very beginning or to the
very end, respectively:
1 my_list = ['a', 'b', 'c', 'b', 'a', 'a']
2 my_list[:3]
91/136
Slicing Lists
We can also specify the step value:
1 my_list = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
2 my_list[1:8:2]
92/136
List Comprehensions
Python provides compact syntax to map one list onto another list. Imagine that we have
a list of numbers and want to create a list with squares of those numbers. We could do
this:
1 numbers = [2, 3, 4]
2 squares = []
3
7 squares
[4, 9, 16]
93/136
List Comprehensions
[4, 9, 16]
94/136
List Comprehensions
We can filter input elements, keeping only those that satisfy a condition:
1 numbers = [2, 3, 4]
2 even_squares = [
3 n ** 2 for n in numbers if n % 2 == 0
4 ]
5 even_squares
[4, 16]
95/136
List Comprehensions
We can loop over nested lists as well:
1 inner_list_1 = [1, 2, 3]
2 inner_list_2 = [4, 5, 6]
3 inner_list_3 = [7, 8, 9]
4 outer_list = [
5 inner_list_1, inner_list_2, inner_list_3
6 ]
7 squares = [
8 n ** 2 for i_l in outer_list for n in i_l
9 ]
10 squares
96/136
Lists: Overview
97/136
Tuples
98/136
More on Tuples
1 my_tuple.count('a') 1 my_tuple[1:-1:2]
3 ('b', 'b')
1 my_tuple.index('c') 1 [ord(n) for n in my_tuple]
2 [97, 98, 99, 98, 97, 97]
99/136
Dictionaries
100/136
More on Dictionaries
We have already seen creating dictionaries and accessing their values by key:
1 my_dict = {'c': 1, 'b': 2, 'a': 3}
2 my_dict['d'] = 4
3 my_dict['c'] = 'Hello, world!'
4 my_dict
KeyError: 'z'
101/136
in and dict.get
If we try to access a key that isn’t in the dictionary, we get a KeyError. We can mitigate
this by either using a check with in (and not in) or by using the dict.get method:
1 my_dict = {'c': 1, 'b': 2, 'a': 3}
2
'default'
1 my_value = my_dict.get('d', 'default')
2 my_value
'default'
102/136
keys, values, and items
To get dictionary’s keys, values, or key‑value pairs, we need to use the corresponding
methods:
1 my_dict = {'c': 1, 'b': 2, 'a': 3}
2 my_dict.keys()
dict_values([1, 2, 3])
1 my_dict.items()
103/136
dict.clear and dict.copy
dict.clear and dict.copy methods act like their list counterparts:
1 my_dict_1 = {'c': 1, 'b': 2, 'a': 3}
2 my_dict_2 = my_dict_1.copy()
3 my_dict_1.clear()
4 my_dict_1
{}
1 my_dict_2
104/136
dict.pop and dict.popitem
105/136
dict.pop and dict.popitem
To remove items from a dictionary, use pop and popitem methods:
1 my_dict
{'c': 1, 'a': 3}
1 my_dict.popitem()
('a', 3)
1 my_dict.popitem()
('c', 1)
1 my_dict.popitem()
106/136
Dictionary Comprehensions
{'H': 72, 'e': 101, 'l': 108, 'o': 111, '!': 33}
107/136
Sets
108/136
More on Sets
Adding items
add
update
Removing items
remove
discard
pop
clear
Manipulating sets
clear
copy
109/136
Set Comprehensions
Set comprehensions allow us to map one set to another in the same fashion:
1 my_set = {5, 4, 3, 2, 1}
2 {x ** 2 for x in my_set if x % 2 != 0}
{1, 9, 25}
110/136
Strings
111/136
More on Strings
'lo ol'
1 my_string[::-1]
'!dlrow ,olleH'
112/136
String Formatting
We can format strings in three different way: the printf‑like formatting via the
%‑operator, the str.format method, and the f‑strings. The printf‑like formatting is pretty
much obsolete, I do not advise using it:
1 '%s is approximately %.2f' % ('Pi', 3.14)
113/136
String Formatting
The str.format method allows us to substitute values by index:
1 '{}, {}, and {}'.format(
2 'first', 'second', 'third'
3 )
114/136
String Formatting
115/136
String Formatting
Pi is an important constant.
Pi is approximately
3.14
116/136
String Formatting
|left |
| center |
| right|
117/136
String Formatting
|Pi | is a constant.
| Pi | is approximately
| 3.14|
118/136
f-strings
f‑strings (strings that start with an f) allow us to embed Python expressions inside
string constants:
1 n = 3
2 p = 4
3 f'{n} to the power of {p} equals {n ** p}'
119/136
When to use what?
f‑strings are evaluated at runtime, so we can use any valid expression inside:
1 f'{n} to the power of {p} equals {n ** p}'
'el,wr'
On the other hand, if we want to create a template (e.g., an HTTP query) and use it with
various values during the program’s life cycle, we probably want to use str.format.
120/136
str methods
Remember to use dir and help! Strings have many useful methods in Python (too
many to cover here). join and split help us to create new strings. lower, upper,
capitalize, and swapcase allow us to manipulate character case. lstrip,
rstrip, and strip remove trailing whitespaces from a string. startswith,
endswith, isalnum and others save us from the hassle of using regular expressions.
'11\t13\t12\t15\t13\t14\t11'
121/136
Containers
122/136
More Containers from collections
The collections module provides several specialized container datatypes. We will cover
namedtuple, Counter, OrderedDict, and deque.
123/136
namedtuple
namedtuple is a complex data type that allows to group variables together under one
name. If we care about grouping attributes but don’t really care about modeling
behavior, we can use a namedtuple instead of defining a new class.
1 from collections import namedtuple
2 ChocoCow = namedtuple('ChocoCow', 'name color cocoa_content')
3 choco_cow = ChocoCow(
4 name='Goldy', color='golden', cocoa_content=65
5 )
6 choco_cow
124/136
namedtuple
125/136
namedtuple
We see an index method in there! Does that mean that we can also access the
properties of our namedtuple by index?
1 choco_cow.name 1 choco_cow[-1]
'Goldy' 65
1 choco_cow[0] 1 choco_cow.cocoa_content
'Goldy' 65
126/136
namedtuple
'golden'
127/136
Counter
Counter({'l': 3, 'o': 2, 'H': 1, 'e': 1, ',': 1, ' ': 1, 'W': 1, 'r': 1, 'd': 1, '!': 1})
128/136
Counter
We can update our existing counter with items from another sequence:
1 my_counter.update('Hallo, Welt!')
2 print(my_counter)
Counter({'l': 6, 'o': 3, 'H': 2, 'e': 2, ',': 2, ' ': 2, 'W': 2, '!': 2, 'r': 1, 'd': 1, 'a': 1, 't': 1})
1 my_counter.most_common(3)
129/136
Counter
130/136
OrderedDict
Starting with Python 3.7, dictionaries preserve the insertion order, as you might have
already noticed:
1 my_dict = {'c': 1, 'b': 2, 'a': 3}
2 my_dict
131/136
OrderedDict
Consider the following example:
1 from collections import OrderedDict
2 ord_dict_1 = OrderedDict({'c': 1, 'b': 2, 'a': 3})
3 ord_dict_2 = OrderedDict({'a': 3, 'b': 2, 'c': 1})
4 dict_1 = {'c': 1, 'b': 2, 'a': 3}
5 dict_2 = {'a': 3, 'b': 2, 'c': 1}
132/136
deque
deque (implemented as a doubly linked list) is a datatype that serves as a
generalization of both a stack and a queue:
1 from collections import deque
2 my_deque = deque([1, 2, 3, 4, 5])
3 my_deque.append(6)
4 my_deque.appendleft(0)
1 my_deque.pop() 1 my_deque.popleft()
6 0
1 my_deque
deque([1, 2, 3, 4, 5])
133/136
deque
deque allows us to efficiently add elements to both ends of the data structure as
opposed to regular Python lists. However, keep in mind that there is no golden pill (or
silver bullet, if you like), and gaining efficiency in some operations probably means
losing efficiency with others. Which operations does a deque perform less efficiently
than a regular list?
134/136
More Containers: Overview
namedtuple
to group variables (properties, attributes) together
Counter
to count collections’ items
OrderedDict
to make sure the insertion order is preserved and checked
deque
to get efficient stack‑like and queue‑like functionality
135/136
Thank you!
QUESTIONS?
136/136