Uitgebreidere Python kennis¶
Python is een batteries included taal. Dat betekent dat als je 'kaal' Python installeert er al heel veel functionaliteit standaard meegeleverd wordt. Allereerst omdat de taal zelf al behoorlijk krachtig is, maar ook omdat de standaardbibliotheek zeer uitgebreid is. Met een eenvoudig import
-statement haal je extra functionaliteit binnen, onder andere op het gebied van datatypes, wiskunde, toegang tot bestanden, een database, datacompressie, cryptografie, netwerktoegang, e-mail, multimedia, etc. Nog veel meer bibliotheken zijn beschikbaar via de Python Package Index17.
In dit hoofdstuk behandelen we de kennis die nuttig kan zijn voor de rest van deze cursus1. Een deel van wat we hier behandelen kan al bekend zijn uit eerdere cursussen. Een ander deel is nieuw.2
In de cursus gaan we bibliotheken (modules, packages) en een applicatie ontwikkelen. Dat betekent dat we verder gaan dan het schrijven van scripts en dat we dus meer gaan doen dan functies schrijven. Uiteindelijk moet het mogelijk zijn de software te verspreiden op een wat meer professionele manier. Dus niet alleen via een zipje met wat Pythonbestanden waar uiteindelijk verschillende versies van rondslingeren en die lastig zijn te updaten. Wat er nodig is voor een goede distributie van software en om het mogelijk te maken met meerdere mensen software te (blijven) ontwikkelen zal in deze cursus aan bod komen.
Een punt wat vaak onderschoven blijft is documentatie. Als je software schrijft die gebruikt (en doorontwikkeld) wordt in een onderzoeksgroep, dan is het heel belangrijk dat iedereen kan begrijpen wat je software doet en hoe die uitgebreid kan worden. Het is zonder hulp vaak heel moeilijk om de code van een iemand anders te begrijpen. En in de praktijk blijkt heel vaak dat als je code schrijft en daar een paar weken of maanden later op terugkijkt, jij zélf die ander bent. Wat toen blijkbaar heel logisch leek, is dat later toch niet meer. Dus documentatie schrijf je heel vaak ook gewoon voor jezelf.
Als je niet zo heel veel in Python geprogrammeerd hebt kan het helpen om de paragraaf Basiskennis Python18 door te nemen. Een boek dat zeker bij natuurkundigen in de smaak kan vallen is Effective Computation in Physics19, maar deze is niet gratis verkrijgbaar. Een boek dat zowel op papier te bestellen is als in de vorm van een pdf of webpagina is te lezen is Think Python.20
Zen of Python¶
Python is niet C (of iedere willekeurige andere programmeertaal). Er zit een gedachte achter die op een gegeven moment verwoord is door Tim Peters21.
Je kunt het lezen middels een easter egg in Python zelf: import this
.
zen
- Open Visual Studio Code.
- Open de map
ECPC
). - Maak een bestand
zen-of-python.py
met daarin de onderstaande code:ECPC
├──zen-of-python.py
└── ••• - Run het script en lees de output.
Deze tekst kan nog behoorlijk cryptisch overkomen, maar een paar dingen worden snel duidelijk: code moet mooi zijn (regel 1) en duidelijk (regels 2, 3 en 6). Er bestaan prachtige programmeertrucs in één of twee regels, maar onleesbaar is het wel. Een voorbeeld 22:
print('\n'.join("%i bytes = %i bits which has %i possible values." %
(j, j*8, 256**j) for j in (1 << i for i in range(4))))
Kun je zien wat de uitvoer van dit programma moet zijn? Misschien als we het op deze manier uitschrijven:
for num_bytes in [1, 2, 4, 8]:
num_bits = 8 * num_bytes
num_possible_values = 2 ** num_bits
print(
f"{num_bytes} bytes = {num_bits} bits which has {num_possible_values} possible values."
)
(ecpc) > python zen.py
1 bytes = 8 bits which has 256 possible values.
2 bytes = 16 bits which has 65536 possible values.
4 bytes = 32 bits which has 4294967296 possible values.
8 bytes = 64 bits which has 18446744073709551616 possible values.
De code is langer, met duidelijkere namen van variabelen en zonder bitshifts of joins.
Moraal van dit verhaal: we worden gelukkiger van code die leesbaar en begrijpelijk is, dan van code die wel heel slim in elkaar zit maar waar bijna niet uit te komen is. Overigens komt het regelmatig voor dat de programmeur zélf een paar weken later al niet zo goed meer weet hoe de code nou precies in elkaar zat.
Als je samenwerkt aan software kan het andere Pythonprogrammeurs erg helpen om dingen 'op de Python-manier te doen'. Een C-programmeur herken je vaak aan het typische gebruik van lijsten of arrays in for
-loops. Als je een lijst hebt: names = ['Alice', 'Bob', 'Carol']
, doe dan niet:
i
. Gebruik liever het feit dat een lijst al een iterator is:
Deze code is bovendien veel korter en gebruikt minder variabelen.
Itereren op de python-manier
- Neem het onderstaande script over.
- Itereer over de lijst
voltages
op de python-manier. - Print voor elk item in de lijst de waarde in mV. Bijvoorbeeld: "The voltage is set to 0 mV."
Uitwerkingen
voltages = [0, 50, 100, 150, 200, 250, 300] #mV
for voltage in voltages:
print(f"The voltage is set to {voltage} mV.")
(ecpc) > python iterator.py
The voltage is set to 0 mV.
The voltage is set to 50 mV.
The voltage is set to 100 mV.
The voltage is set to 150 mV.
The voltage is set to 200 mV.
The voltage is set to 250 mV.
The voltage is set to 300 mV.
Enumerate¶
Soms is het nodig om de index te hebben, bijvoorbeeld wanneer je een namenlijstje wilt nummeren:
Dit kan dan in Python-code het makkelijkst als volgt:
Hier maken we gebruik van deenumerate(iterable, start=0)
-functie en f-strings. Er zijn dus veel manieren om programmeerproblemen op te lossen, maar het helpt om het op de `Pythonmanier' te doen. Andere programmeurs zijn dan veel minder tijd en energie kwijt om jouw code te begrijpen -- én andersom wanneer jij zelf op internet zoekt naar antwoorden op problemen. Immers, je herkent dan veel makkelijker en sneller hoe andermans code werkt.
Datatypes¶
Gehele getallen, kommagetallen, strings: allemaal voorbeelden van datatypes. Veel zullen jullie al wel bekend voorkomen, zoals strings, lists en NumPy arrays. Andere zijn misschien alweer wat weggezakt, zoals dictionaries of booleans. Weer andere zijn misschien wat minder bekend, zoals complexe getallen of sets. En als laatste voegt Python af en toe nieuwe datatypes toe, zoals f-strings in Python 3.6 of data classes sinds Python 3.7.
Info
De python-standard-library documentatie 23 bevat een mooi overzicht van alle datatypes met een beschrijving van operaties en eigenschappen. Voor uitgebreidere tutorials kun je vaak terecht bij real-python 24. Het kan makkelijk zijn om in een zoekmachine bijvoorbeeld real python dict
te typen als je een tutorial zoekt over Python dictionaires.
Om nog even te oefenen met de datatypes volgt er een aantal korte opdrachten.
List¶
list
Schrijf een kort scriptje.
- Maak een
list
van de wortels van de getallen 1 tot en met 10. Dus de rij $\left(\sqrt{1}, \sqrt{2}, \sqrt{3}, \ldots, \sqrt{10}\right)$. - Print die rij onder elkaar (één getal per regel, met drie decimalen).
- Geef weer of het getal 3 voorkomt in die rij en geef weer of het getal 4 voorkomt in die rij.
Uitwerkingen
import math
squares = []
for n in range(1, 11):
squares.append(math.sqrt(n))
# print the list below each other with three decimal places
print("Square of range 1 to 10 with three decimal places: ")
for square in squares:
print(f"{square:.3f}")
# State if number 3 or 4 appears in the list of squares
for number in [3, 4]:
print(f"does number {number} appears in the list of squares?", number in squares)
(ecpc) > python list.py
Square of range 1 to 10 with three decimal places:
1.000
1.414
1.732
2.000
2.236
2.449
2.646
2.828
3.000
3.162
does number 3 appears in the list of squares? True
does number 4 appears in the list of squares? False
NumPy array¶
Je kunt op verschillende manieren een NumPy array maken (na import numpy as np
):
- Door een Python lijst te converteren:
np.array([0, 0.5, 1, 1.5, 2])
. - Door een array aan te maken met een stapgroote:
np.arange(0, 3, 0.5) # start, stop, step
- Door een array aan te maken met getallen gelijkmatig verdeeld over een interval:
np.linspace(0, 2.5, 6) #start, stop, number
NumPy arrays
NumPy arrays zijn vaak handiger dan lists. Als je een array hebt van 20 $x$-waardes in het domein $[0, \pi]$ kun je in één keer alle waardes van $\sin x$ uitrekenen. Bijvoorbeeld:
NumPy voert de berekeningen uit binnen een C-bibliotheek3 en is daarmee veel sneller dan een berekening in Python zelf: Niet alleen is NumPy zo'n honderd keer sneller,4 het is ook veel korter op te schrijven. Het nadeel van NumPy arrays is dat je geen elementen kunt toevoegen.5 Python lijsten hebben dus voordelen, zeker als rekentijd geen probleem voor je is.Als je veel functies uit NumPy gebruikt is het handig – en gebruikelijk – om je import-statements kort te houden en duidelijk te maken dat je de sin()
-functie uit NumPy gebruikt en niet uit de math
module. Constantes worden wel vaak los geïmporteerd. Daarom is dit dus gebruikelijk:
np.array
Doe hetzelfde als de vorige opdracht met lists, maar nu met NumPy arrays:
- Maak een
np.array
van de wortels van de getallen 1 tot en met 10. Dus de rij $\left(\sqrt{1}, \sqrt{2}, \sqrt{3}, \ldots, \sqrt{10}\right)$. - Print die rij onder elkaar (één getal per regel, met drie decimalen).
- Geef weer of het getal 3 voorkomt in die rij en geef weer of het getal 4 voorkomt in die rij.
Uitwerkingen
import numpy as np
# Make an array from 1 to 10
numbers = np.arange(1, 11, 1)
# Take the squareroot of each number
squareroot = np.sqrt(numbers)
# Print the list of squareroots below each other with three decimal places
for root in squareroot:
print(f"{root:.3f}")
# State if number 3 or 4 appears in the list of squares
print("does number 3 appears in the list of squares?", 3 in squareroot)
print("does number 4 appears in the list of squares?", 4 in squareroot)
(ecpc) > python np_array.py
1.000
1.414
1.732
2.000
2.236
2.449
2.646
2.828
3.000
3.162
does number 3 appears in the list of squares? True
does number 4 appears in the list of squares? False
Dictionaries
Dictionaries¶
Dictionaries zijn een bijzonder handige manier om informatie op te slaan. Een dictionary bestaat uit een of meerdere key-value tweetallen. Met een handige gekozen naam voor de key kan je betekenis geven aan een value.
dict
Schrijf een kort scriptje:
- Maak een dictionary
constants
met de waardes van de (natuur)constantes $\pi$, de valversnelling $g$, de lichtsnelheid $c$ en het elementaire ladingskwantum $e$. - Print de namen -- niet de waardes -- van de constantes die zijn opgeslagen in
constants
. - Bereken de zwaartekracht $F_\text{z} = mg$ voor een voorwerp met een massa van 14 kg door gebruik te maken van de waarde van $g$ uit de dictionary.
- Maak een dictionary
measurement
die de resultaten van een meting bevat: een spanning van 1.5 V bij een stroomsterkte van 75 mA. - Bereken de weerstand van de schakeling op basis van de voorgaande meting en bewaar het resultaat in dezelfde dictionary.
Uitwerkingen
import numpy as np
# Dictionary of constants pi, gravitational acceleration (g), the speed of light (c) and elementary charge (e)
constants = {"pi": np.pi, "g": 9.81, "c": 3e8, "e": 1.6e-19}
# print de names -not the values- of the constants in the dictionary
print(constants.keys())
# Calculate gravity of an object with mass of 14 kg
mass = 14 # kg
F_z = mass * constants["g"]
print(f"Gravity of an object with {mass} kg is: {F_z} N")
# Dictionary with results of a measurement
measurement = {"U": 1.5, "I": 75e-3} # U in V, I in A
# Add resistance to dictionary
measurement["R"] = measurement["U"] / measurement["I"]
print(f'The resistance was: {measurement["R"]:.2f} \u03A9')
(ecpc) > python dictionaries.py
dict_keys(['pi','g', 'c', 'e'])
Gravity of an object with 14kg is: 137.34 N
The resistance was: 20.00 Ω
Tuples, * args, ** kwargs
Tuples, * args, ** kwargs¶
In Python zijn tuple
's een soort alleen-lezen
list
's. Een tuple is een immutable6 object. Daarom worden ze vaak gebruikt wanneer lijstachtige objecten altijd dezelfde vorm moeten hebben. Bijvoorbeeld een lijst van $(x, y)$-coördinaten zou je zo kunnen definiëren:
coords[0]
gelijk aan (0, 0)
. Je kunt nu niet dit coördinaat uitbreiden naar drie dimensies met coords[0].append(1)
en dat is waarschijnlijk precies wat je wilt voor een lijst met tweedimensionale coördinaten. Ook is dit object veel compacter dan een dict
:
Hier zijn tuples dus best handig, al moet je dus wel onthouden in welke volgorde de elementen staan. Dat is voor $(x, y)$-coördinaten niet zo'n probleem maar kan in andere situaties lastiger zijn.7 Tuples ondersteunen tuple unpacking. Je kunt het volgende doen:
Na deze operatie geldt $x = 2$, $y = 3$ en $z = 4$. Je mag zelfs de haakjes weglaten voor nog compactere notatie:
Op deze manier kan een functie ook meerdere argumenten teruggeven die je vervolgens uit elkaar plukt:
def get_measurement():
... # perform measurement
return voltage, current
voltage, current = get_measurement()
def power(a, b):
return a ** b
# regular function call
power(2, 7)
# function call with tuple unpacking
args = 2, 7
power(*args)
# regular function call
power(b=7, a=2)
# function call with dictionary unpacking
kwargs = {"b": 7, "a": 2}
power(**kwargs)
args
Gegeven de lijst odds = [1, 3, 5, 7, 9]
, print de waardes uit deze lijst op één regel, zoals hieronder weergegeven:
Set
Set¶
Als laatste willen we nog de aandacht vestigen op set
's: een unieke verzameling van objecten. Ieder element komt maar één keer voor in een set:
{}
-haakjes worden gebruikt voor zowel sets als dictionaries. Omdat een dictionary (key: value) paren heeft en een set losse elementen kan Python het verschil wel zien:
Dat gaat alleen mis als je een lege set wilt maken. Daarvoor zul je expliciet de set()
-constructor moeten gebruiken:
Je kunt elementen toevoegen aan een set met .add()
en sets gebruiken om verzamelingen met elkaar te vergelijken. Komen er elementen wel of niet voor in een set? Is de ene set een subset van de andere set? Enzovoorts. Zie daarvoor verder de documentatie.
Comprehension¶
Door gebruik te maken van een list comprehension kun je de for-loop in één regel opschrijven:
Er is in veel gevallen tegenwoordig geen groot verschil met een for-loop qua snelheid. In andere gevallen is de list comprehension net wat sneller. Als je lijsten niet te lang zijn is het makkelijker (en sneller) om een list comprehension te gebruiken in plaats van je lijst éérst naar een array te veranderen en er dan mee verder te rekenen. Als je lijst wél lang is of je weet al dat je meerdere berekeningen wilt uitvoeren kan dat wel:Kortom: berekeningen met arrays zijn sneller, maar for-loops (en list comprehensions) zijn veelzijdiger. Het is zelfs mogelijk om een if
-statement op te nemen in je list comprehension. Bijvoorbeeld:
filenames = ["test.out", "text.pdf", "manual.pdf", "files.zip"]
pdfs = [name for name in filenames if name.endswith(".pdf")]
print(f"{pdfs=}")
(ecpc) > python pdf.py
pdfs=['text.pdf', 'manual.pdf']
In een for-loop heb je daar meer ruimte voor nodig. Naast list comprehensions heb je ook set comprehensions8 en dict comprehensions.
array, for-loops en comprehensions
Voer, door een script te schrijven, de volgende opdrachten uit:
- Maak een lijst van de getallen 1 tot en met 10.
- Gebruik een 'gewone' for-loop om een lijst te maken van de derdemachtswortel van de getallen.
- Maak nogmaals een lijst van de derdemachtswortel van de getallen maar gebruik nu list comprehension.
- Gebruik tot slot arrays om de lijst met derdemachtswortels van de getallen te maken.
Uitwerkingen
import numpy as np
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# use a for loop to create a list with cube root of numbers
cube_root = []
for number in numbers:
answer = number ** (1 / 3)
cube_root.append(answer)
# use list comprehension to create a list with cube root of numbers
cube_root_comprehension = [n ** (1 / 3) for n in numbers]
# use numpy arrays to create a list with cube root of numbers
numbers = np.array(numbers)
cube_root_array = numbers ** (1 / 3)
print(cube_root)
print(cube_root_comprehension)
print(cube_root_array)
(ecpc) > python for_loop.py
[1.0, 1.2599210498948732, 1.4422495703074083, 1.5874010519681994, 1.7099759466766968, 1.8171205928321397, 1.912931182772389, 2.0, 2.0800838230515904, 2.154434690031884]
[1.0, 1.2599210498948732, 1.4422495703074083, 1.5874010519681994, 1.7099759466766968, 1.8171205928321397, 1.912931182772389, 2.0, 2.0800838230515904, 2.154434690031884]
[1. 1.25992105 1.44224957 1.58740105 1.70997595 1.81712059 1.91293118 2. 2.08008382 2.15443469]
Lambda functions
Lambda functions¶
In Python zijn functies ook objecten. Je kunt ze bewaren in een lijst of dictionary, of je kunt ze meegeven als parameter aan een andere functie. Dat kan heel handig zijn! Stel je hebt een lijst met verschillende soorten fruit die je wilt sorteren op alfabet:
(ecpc) > python sort.py
['apple', 'banana', 'kiwi']
Dat gaat heel makkelijk met de ingebouwde sorted()
-functie. Je kunt aan deze functie ook een key
-parameter meegeven; een ándere functie die gebruikt wordt om te bepalen waarop gesorteerd moet worden. Zo kun je sorteren op de lengte van de fruitnamen door simpelweg de len()
-functie als parameter mee te geven:
(ecpc) > python length.py
5
['kiwi', 'apple', 'banana']
Als je wilt sorteren op de tweede letter van de naam -- waarom niet? -- dan kun je zelf een functie definiëren en gebruiken:
a = ["kiwi", "banana", "apple"]
def second_letter(value):
return value[1]
print(second_letter("lemon"))
print(sorted(a, key=second_letter))
(ecpc) > python second_letter.py
e
['banana', 'kiwi', 'apple']
Lambdafuncties zijn bedacht om je een hoop typewerk te besparen. Je kunt korte functies in één regel opschrijven en gebruiken, zolang het maar een geldige expression is. Géén if-then-else, maar de meeste andere dingen mogen wel. Bijvoorbeeld:
a = ["kiwi", "banana", "apple"]
squared = lambda x: x ** 2
print(squared(4))
second_letter = lambda value: value[1]
print(sorted(a, key=second_letter))
(ecpc) > python lamda.py
16
['banana', 'kiwi', 'apple']
Aangezien de definitie van een lambdafunctie zelf ook een expression is kun je het sorteren op de tweede letter zelfs in één regel doen:
(ecpc) > python one_line.py
['banana', 'kiwi', 'apple']
Lambdafuncties kun je ook gebruiken om te fitten aan een bepaald model. Je definieert je model dan in één regel met een lambdafunctie:
# from lmfit import models
f = lambda x, a, b: a * x + b
model = models.Model(f)
fit = model.fit(y, x=x)
lmfit
er vanuit gaat dat de eerste variabele in de functiedefinitie de onafhankelijke variabele ($x$-as) is. Dit is verder geen Pythonlimitatie.
Je kunt de functies ook bewaren in een dictionary voor later gebruik.
lambda
Maak een dictionary models
met functies voor een lineaire functie linear
gegeven door $y = ax + b$, een kwadratische functie quadratic
gegeven door $y = ax^2 + bx + c$ en een sinusfunctie sine
gegeven door $a + b\sin(cx + d)$. Hierna moet de volgende code werken:
Uitwerkingen
import numpy as np
from numpy import pi
import matplotlib.pyplot as plt
# dictionary with linear, quadratic and sine function
models = {
"linear": lambda x, a, b: a * x + b,
"quadratic": lambda x, a, b, c: a * x ** 2 + b * x + c,
"sine": lambda x, a, b, c, d: a + b * np.sin(c * x + d),
}
# test the next piece of code
f = models["linear"]
print(f(5, a=2, b=3))
# Graph of sine function on domain [0, 2pi] with parameters a=1, b=2, c=2, d=0.5pi
x = np.linspace(0, 2 * pi, 100)
f = models["sine"]
plt.plot(x, f(x, a=1, b=2, c=2, d=0.5 * pi))
plt.show()
Generators
Generators¶
Als een functie een serie metingen verricht kan het lang duren voordat de functie de resultaten teruggeeft. Laten we die functie even perform_measurements()
noemen. Het is soms lastig als de rest van het programma daarop moet wachten voordat een analyse kan worden gedaan, of een melding aan de gebruiker kan worden gegeven. Het kan dan gebeuren dat je je programma draait en je dan afvraagt: doet hij het, of doet hij het niet?
Je kunt dit oplossen door print()
-statements in je programma op te nemen, maar dit is niet zo netjes. Als je perform_measurements()
inbouwt in een tekstinterface die ook stil
moet kunnen zijn? Of als je de functie gaat gebruiken vanuit een grafisch programma waarin je geen tekst wilt printen, maar een grafiek wilt opbouwen? Je moet dan steeds perform_measurements()
gaan aanpassen. Een ander probleem kan optreden wanneer je langdurige metingen doet die ook veel geheugen innemen. Wachten op de hele meetserie betekent dat het geheugen vol kan lopen. Lastig op te lossen!
Of… je maakt gebruik van een generator function: een functie die tussendoor resultaten teruggeeft. Dat kan door gebruik te maken van yield
in plaats van return
. De rest gaat automatisch. Maar: je moet wel even weten hoe je omgaat met de generator. Stel, we willen de kwadraten berekenen van een reeks getallen tot een bepaald maximum:
def calculate_squares_up_to(max_number):
"""Calculate squares of all integers up to a maximum number"""
squares = []
for number in range(max_number):
squares.append(number ** 2)
return squares
print(calculate_squares_up_to(5))
(ecpc) > python squares.py
[0, 1, 4, 9, 16]
De functie berekent eerst alle kwadraten, voegt ze toe aan een lijst en geeft vervolgens de lijst met uitkomsten terug. Een generator definieer je als volgt:
def calculate_squares_up_to(max_number):
"""Generate squares of all integers up to a maximum number"""
for number in range(max_number):
yield number ** 2
next()
, als volgt:
square_generator = calculate_squares_up_to(5)
next(square_generator)
# 0
next(square_generator)
# 1
...
next(square_generator)
# 16
next(square_generator)
# StopIteration
StopIteration
exception en crasht het programma -- tenzij je de exception afvangt. Het werkt, maar het is niet helemaal ideaal. Makkelijker is om de generator te gebruiken in een loop:
(ecpc) > python squares.py
Still calculating...
0
Still calculating...
1
Still calculating...
4
Still calculating...
9
Still calculating...
16
Dit kan ook in list comprehensions. En als je toch wilt wachten op alle resultaten, dan kan dat eenvoudig met squares = list(calculate_squares_up_to(5))
.
generators
Schrijf een generator function die het vermoeden van Collatz illustreert. Dat wil zeggen: beginnend bij een getal $n$, genereer het volgende getal als volgt: is het getal even, deel het dan door twee; is het getal oneven, vermenigvuldig het met 3 en tel er 1 bij op. Enzovoorts. Sluit de generator af als de uitkomst gelijk is aan 1. Dat is het vermoeden van Collatz: ongeacht met welk geheel getal je begint, je komt altijd op 1 uit. Als voorbeeld, beginnend bij het getal 3 krijg je de reeks 3, 10, 5, 16, 8, 4, 2, 1.
Uitwerkingen
def Collatz(x):
"""Illustrates Collatz's conjecture
Starts with x generates next number when x is not equal to 1.
Next number is x devided by 2 if x is even and x times 3 + 1 if x is odd.
Collatz suspects that you always end with number 1 despite of the starting value.
Args:
x (int): starting value
Yields:
int: next number in the sequence
"""
yield x
while x != 1:
if x % 2 == 0:
# x is even and is divided by 2
x = x // 2 # dubble // makes it an integer
else:
# x is odd, multiply by 3 and add 1
x = 3 * x + 1
yield x
print("print the values of generator with next:")
collatz_generator = Collatz(3)
print(next(collatz_generator))
print(next(collatz_generator))
print(next(collatz_generator))
print(next(collatz_generator))
print(next(collatz_generator))
print(next(collatz_generator))
print(next(collatz_generator))
print(next(collatz_generator))
# print(next(collatz_generator)) # gives StopIteration exception
print("print values of generator without next:")
for number in Collatz(28):
print(number)
(ecpc) > python collatz.py
print the values of generator with next:
3
10
5
16
8
4
2
1
print the values of generator without next:
28
14
7
22
11
34
17
52
26
13
40
20
10
5
16
8
4
2
1
Dunder methods
Dunder methods¶
Hoe weet Python eigenlijk wat de lengte is van een string? Of hoe je getallen optelt? Voor operatoren als + - * / **
wordt eigenlijk een method aangeroepen. bijvoorbeeld __add__()
voor +
, en __mul__()
voor *
. Een ingebouwde functie als len()
roept stiekem de method __len__()
aan en print()
print de uitvoer van __str__()
. Zulke methodes worden dunder methods9 of magic methods genoemd. We kunnen zelf bijvoorbeeld een vector introduceren waarbij we de operatoren voor onze eigen doeleinden gebruiken 25. We definiëren het optellen van vectoren en de absolute waarde (norm) van de vector:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
new_x = self.x + other.x
new_y = self.y + other.y
return Vector(new_x, new_y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** .5
__init__()
methode zorgt voor de initialisatie van de class en de eerste parameter die alle methodes meekrijgen verwijst naar zichzelf en wordt dus gewoonlijk self
genoemd.10 Met de regel self.x = x
wordt de parameter x
bewaard voor later gebruik. Je kunt de class gebruiken op de volgende manier:
>>> v1 = Vector(0, 1)
>>> v2 = Vector(1, 0)
>>> abs(v1)
1.0
>>> abs(v2)
1.0
>>> abs(v1 + v2)
1.4142135623730951
>>> (v1 + v2).x, (v1 + v2).y
(1, 1)
>>> v1 + v2
<__main__.Vector object at 0x7fdf80b3ae10>
>>> print(v1 + v2)
<__main__.Vector object at 0x7fdf80b45450>
__str__()
, gebruikt door str()
en dus ook print()
, en __repr__()
, gebruikt door repr()
en de Python interpreter.12
class Vector:
...
def __repr__(self):
return f"Vector: ({self.x}, {self.y})"
def __str__(self):
# roept __repr__ aan
return repr(self)
__str__
en __repr__
te definiëren.
Vaak hebben classes geen dunder methods nodig (behalve __repr__
en __str__
).
Decorators
Decorators¶
Functies zijn ook objecten in Python. Je kunt ze, zoals we eerder gezien hebben, meegeven als argument of bewaren in een dictionary. Ook kun je functies in functies definiëren en functies definiëren die functies teruggeven. Vaag13. Ik moet hier altijd weer even over nadenken en daarom mag je dit stukje overslaan. Om decorators te gebruiken, hoef je niet per se te weten hoe ze werken.
Decorators worden vaak gebruikt om het gedrag van een functie aan te passen.
Stel je hebt een functie die eenvoudig twee getallen vermenigvuldigd. Je wilt deze functie, zonder hem van binnen te veranderen, aanpassen zodat hij altijd het kwadraat van de vermenigvuldiging geeft. Dus niet $a\cdot b$, maar $(a\cdot b)^2$. Dat kan als volgt:
def f(a, b):
return a * b
def squared(func, a, b):
return func(a, b) ** 2
f(3, 4)
# 12
squared(f, 3, 4)
# 144
squared()
aan te roepen en dan óók nog de functie f()
als eerste argument mee te geven. Lastig. Maar omdat functies objecten zijn kan dit ook:
Hier gebeurt iets geks… Om te begrijpen wat hier gebeurt moeten we een beetje heen en weer springen. In regel 8 roepen we de functie squared_func(f)
aan. In regel 5 zien we dat die functie een andere functie teruggeeft -- die niet wordt aangeroepen! In regel 8 wordt die functie bewaard als g
en pas in regel 9 roepen we hem aan. De functie g()
is dus eigenlijk gelijk aan de functie inner_func()
die in regels 2--3 gedefinieerd wordt. De aanroep in regel 9 zorgt er uiteindelijk voor dat in regel 3 de oorspronkelijke functie f(a, b)
wordt aangeroepen en dat het antwoord gekwadrateerd wordt. Dit is echt wel even lastig.
In deze opzet moet de inner_func(a, b)
nog weten dat de oorspronkelijke functie aangeroepen wordt met twee argumenten a
en b
. Maar ook dat hoeft niet. We hebben immers argument (un)packing met *args
:
Als je meer wilt weten over hoe decorators werken en hoe je je eigen decorators kunt maken, dan vind je een uitgebreide uitleg in Primer on Python Decorators 26. Deze tutorial heb je niet per se nodig voor de volgende opdracht.
decorators
Schrijf en test een decorator die werkt als een soort logboek. Als je een functie aanroept die gedecoreerd is print dan een regel op het scherm met het tijdstip van de aanroep, de parameters die meegegeven werden én de return value van de functie.
Uitwerkingen
import datetime
def log(func):
def inner(*args, **kwargs):
return_value = func(*args, **kwargs)
print(40 * "-")
print(f"Logging function call at {datetime.datetime.now()}.")
print(f"Function was called as follows:")
print(f"Arguments: {args}")
print(f"Keyword arguments: {kwargs}")
print(f"And the return value was {return_value}")
print(40 * "-")
return return_value
return inner
@log
def f(a, b):
return a * b
f(3, 4)
f(3, b=4)
(ecpc) > python decorators.py.py
----------------------------------------
Logging function call at year-month-date hours:minutes:seconds
Function was called as follows:
Arguments: (3, 4)
Keyword arguments: {}
And the return value was 12 ---------------------------------------- ----------------------------------------
Logging function call at year-month-date hours:minutes:seconds
Function was called as follows:
Arguments: (3,)
Keyword arguments: {'b': 4}
And the return value was 12
----------------------------------------
Modules¶
Als je een nieuw script begint te schrijven staat alle code in één bestand. Dat is lekker compact, maar heeft ook nadelen. Als je je experiment of programma gaat uitbreiden kan het erg onoverzichtelijk worden. Ook zul je al je wijzigingen steeds in dit bestand moeten doen terwijl je je code van eerdere experimenten misschien wel wilt bewaren. Mogelijk kopieer je steeds je script naar een nieuw bestand, maar dat is niet erg DRY.14 Als je dan bijvoorbeeld een functie of class wilt aanpassen, moet dat nog steeds op heel veel plekken. Daarom is het handig om gebruik te maken van modules.
Eenvoudig gezegd is een module een stuk Python code dat je kunt importeren en gebruiken. Meestal worden er in een module handige functies en classes gedefinieerd:
(ecpc) > python math.py
1.4142135623730951
3.141592653589793
1.0
Door de math
module te importeren hebben we opeens de beschikking over het getal $\pi$ en de sinus- en wortelfunties.
Je kunt je eigen code ook importeren, maar hier moet je wel even opletten. Stel, we hebben een bestand square.py
:
(ecpc) > python square.py
The square of 4 is 16
De uitvoer is zoals verwacht. Maar nu willen we in een nieuw script, count_count.py
, de functie importeren en gebruiken:
(ecpc) > python count_count.py
The square of 4 is 16
The square of 5 is 25
square.square
Waarom staat er in bovenstaande code nu opeens square.square()
in plaats van gewoon square()
?
Uitwerkingen
Omdat je uit de module square.py
de functie square()
gebruikt.
Maar nu is er een probleem met de uitvoer van dit script: zowel het kwadraat van 4 als van 5 wordt geprint.
Tijdens het importeren wordt alle code die aanwezig is in square.py
ook daadwerkelijk gerund. Er zijn twee manieren om dit op te lossen:
- Alle 'extra' code verwijderen uit de module (
square.py
) - De code in de module alleen laten runnen als de module als script wordt aangeroepen, maar niet wanneer de module wordt geïmporteerd
De eerste oplossing is lang niet altijd wenselijk. Voor de tweede oplossing pas je square.py
als volgt aan:
def square(x):
return x**2
if __name__ == "__main__":
print(f"The square of 4 is {square(4)}")
__name__
gelijk aan de string __main__
. Maar als je een module importeert is
__name__
gelijk aan de naam van de module; in dit geval square
. Met bovenstaande constructie wordt de code alleen uitgevoerd wanneer de module direct gerund wordt:
(ecpc) > python square.py
The square of 4 is 16
(ecpc) > python count_count.py
The square of 5 is 25
Het if __name__ == '__main__'
-statement wordt heel veel gebruikt in Python modules.
modules
- Maak zelf de bestanden
square.py
enjust_count.py
aan.
ECPC
├──square.py
├──just_count.py
└── ••• - Run
just_count.py
zonder hetif __name__ == '__main__'
-statement. - Run
just_count.py
met hetif __name__ == '__main__'
-statement. - Voeg
print(f"{__name__ = }")
toe bovenaansquare.py
. - Run
square.py
en kijk wat__name__
is. - Run dan nu
just_count.py
. Zie hoe de speciale variabele__name__
verandert.
Packages¶
In Python zijn packages collecties van modules. Ook krijg je automatisch namespaces. Dat wil zeggen, wanneer je functies en modules uit een package importeert zitten ze niet in één grote vormeloze berg, maar in een soort boomstructuur. Dat betekent dat namen niet uniek hoeven te zijn. Er zijn duizenden bibliotheken beschikbaar voor python (numpy
, scipy
, matplotlib
, etc.) en die mogen allemaal een module test
bevatten. Namespaces zorgen ervoor dat je ze uniek kunt benaderen:
numpy
en scipy
afzonderlijke namespaces. Ook zijn numpy.test
en scipy.test
afzonderlijke namespaces. De namen van bijvoorbeeld variabelen en functies binnen die modules zullen nooit met elkaar in conflict komen.
Wij gaan in deze cursus onze code ook in packages stoppen. Op die manier kun je een softwarebibliotheek opbouwen voor je experiment en die code makkelijker delen met andere onderzoekers. Een pakket is opgebouwd zoals hieronder weergegeven:
└── my_project_folder
├── script.py
└── my_package
├── __init__.py
└── package1
├── __init__.py
├── module1.py
└── module2.py
└── package2
├── __init__.py
└── module3.py
└── module4.py
Iedere package bestaat uit een directory met een __init__.py
-bestand.15
De verschillende modules uit het figuur hierboven kun je als volgt importeren en gebruiken in het bestand script.py
(we gaan er even vanuit dat iedere module een functie some_function()
bevat):
# module direct importeren
import my_package.package1.module1
my_package.package1.module1.some_function()
# losse module vanuit een package importeren
from my_package.package1 import module2
module2.some_function()
# module importeren onder een andere naam
import my_package.module4 as m4
m4.some_function()
In deze cursus gaan we ook packages maken. Feitelijk hoeven we een python script dus alleen maar in een map te stoppen en in diezelfde map een lege __init__.py
aan te maken.
Waarschuwing
Let op: als je de __init__.py
vergeet dan lijkt alles het alsnog te doen. Maar je maakt nu een implicit namespace package waarbij bepaalde directories toch weer op een grote hoop gegooid worden. Geloof me, echt niet handig.16 Namespace packages kunnen handig zijn voor grote projecten, maar dat is het dan ook wel. Wij gaan hier niet verder op in. Kortom: let op en gebruik altijd een __init__.py
.
Packages
In deze opdracht ga je oefenen met het aanmaken van packages, modules en het importeren en aanroepen daarvan.
- Maak in de map
ECPC
een packagemodels
met twee modules:polynomials.py
entests.py
. - In de
polynomials
-module maak je een functieline(x, a, b)
die de vergelijking voor een lijn voor ons berekent: $y = ax + b$. -
In de
tests
-module maak je een functietest_line()
die het volgende doet:- gebruik de
line()
-functie uit depolynomials
-module om de $y$-waarde uit te rekenen voor een bepaald punt bij een gegeven $a$ en $b$. - Vergelijk die berekende waarde met de waarde die het volgens jou moet zijn (met de hand nagerekend).
- Print
TEST PASSED
als het klopt, enTEST FAILED
als het niet klopt.
- gebruik de
-
Maak een bestand
practice-packages.py
die:- Een grafiek maakt van jouw lijn. Bepaal zelf het domein en de waardes voor $a$ en $b$.
- De test uitvoert door de
test_line()
-functie aan te roepen. - Pas je
line()
-functie eventjes aan om te kijken of je test ook echt werkt. Bijvoorbeeld: bij $y = ax$ zou jeTEST FAILED
moeten zien.
Uitwerkingen
De mappen structuur ziet er als volgt uit:
└── ECPC
├── •••
├── practice-packages.py
└── models
├── __init__.py
├── polynomials.py
└── tests.py
Relatieve en absolute imports
Als je in een module een andere module wilt importeren dan zijn daarvoor twee opties: relatieve en absolute imports. Relatief wil zeggen: importeer module1 uit dezelfde directory, of ten opzichte van deze directory (..
betekent een directory hoger bijvoorbeeld). Bij een absolute import moet je de volledige locatie binnen het package opgeven. Als voorbeeld, stel dat module1
uit het figuur hierboven de modules module2
en module3
wil importeren:
De Standard Library en de Python Package Index
De Standard Library en de Python Package Index¶
Voor Python zijn ontzettend veel bibliotheken beschikbaar die het leven een stuk aangenamer maken. Voor een gedeelte daarvan geldt dat ze altijd aanwezig zijn als je Python geïnstalleerd hebt. Deze set vormt de standard library 23. Om te voorkomen dat je zelf het wiel uitvindt is het goed om af en toe door de lijst te bladeren zodat je een idee krijgt wat er allemaal beschikbaar is. Ziet het er bruikbaar uit? Lees dan vooral de documentatie! Tip: vergeet de built-in functions niet.
Standard Library
- Zoek de The Python Standard Library lijst op in bijvoorbeeld de Python documentatie.
- Welke bibliotheken heb je al eerder gebruik van gemaakt?
- Kies een bibliotheek uit die jouw aandacht trekt en neus door de documentatie.
- Ga terug naar de lijst en bekijk de Built-in functions, welke functie kende je nog niet maar lijkt je wel heel handig?
Verder zijn er nog eindeloos veel packages beschikbaar gesteld door programmeurs, van hobbyist tot multinational. Deze kunnen centraal gepubliceerd worden in de Python Package Index 17. Je kunt daar vaak ook zien hoe populair een package is. Dit is een belangrijke indicatie voor de kwaliteit en bruikbaarheid van een package.
Exceptions¶
Exceptions zijn de foutmeldingen van Python. Je krijgt ze als je bijvoorbeeld probeert te delen door nul
(ecpc) > python divide.py
Traceback (most recent call last):
File "devide.py", line 1, in < module >
print(1/0)
~^~
ZeroDivisionError: division by zero
of wanneer je een typefout maakt:
(ecpc) > python particle.py
Traceback (most recent call last):
File "particle.py", line 2, in < module >
s.upler()
^^^^^^^
AttributeError: 'str' object has no attribute 'upler'. Did you mean: 'upper'?
Merk op dat je een exception met traceback meestal van onder naar boven leest. Onderaan staat de foutmelding (exception) en daar boven een traceback: een kruimelpad van wáár in de code het probleem optrad; onderaan de regel waarin het echt fout ging, en naar boven toe alle tussenliggende functies en bibliotheken met bovenaan het hoofdprogramma.
Exception
number_input = input("Give a number: ")
number_multiply = 2.8
print(number_input * number_multiply)
- Neem het script hierboven over en voer het uit.
- Wat voor soort error geeft Python terug?
- In welke regel van het script zit volgens de Traceback het probleem?
- Leg in eigen woorden uit wat het probleem is.
Uitwerkingen
- Python geeft een TypeError terug.
- Het probleem zit in regel 3:
print(number_input * number_multiply)
- De functie
input()
geeft een string terug. Daardoor isnumber_input
ook een string. Een string behoort tot het type sequences. Het is een reeks van elementen, '28' is een reeks van '2' en '8', 'abc' is een reeks van 'a', 'b' en 'c' en ook [0,1,2] is reeks van '0', '1', '2'. Zelfs als je in dit script het getal 8 zou invoeren dan is number_input een sequence met maar een element: '8'. Een sequence kan je vermenigvuldigen, maar niet met en float, alleen met een interger. Kijk maar eens wat er gebeurd als jenumber_multiply = 3
neerzet. Wat gebeurd er als je 'abc' met 3 vermenigvuldigd? En kun je ook 3 * [0,1,2] printen?
Exceptions afvangen
Een exception kan vervelend zijn. Het is een beetje jammer als je bijvoorbeeld tijdens een langdurige meting telkens een weerstand aan het uitrekenen bent ($R = \frac{U}{I}$) en de stroomsterkte $I$ wordt na anderhalf uur heel eventjes nul. Je programma crasht en je metingen zijn weg. Zoek de fout (niet altijd makkelijk!) en probeer het nog eens.
Je kunt exceptions afvangen en afhandelen met een try...except
blok:
Ook kun je zelf exceptions maken. Stel je schrijft een programma om een oscilloscoop uit te lezen dat twee kanalen heeft om de spanning te meten. Kanaal 0 en kanaal 1. Het programma moet gebruikt kunnen worden door andere studenten in de onderzoeksgroep dus het kan nu eenmaal gebeuren dat iemand niet op zit te letten -- niet jij, jij let altijd goed op. Een andere student die een programma schrijft en jouw code gebruikt wil een spanning meten op kanaal 2, het was immers een tweekanaals oscilloscoop. Maar kanaal 2 bestaat niet. Sommige oscilloscopen klagen dan niet maar geven een random getal terug. Dit kan leiden tot heel vervelende en lastig te achterhalen fouten in het experiment. Met dat idee in je achterhoofd kun je code schrijven die controleert op het kanaalnummer en een exception geeft:
# we maken een subclass van de 'standaard' Exception
class InvalidChannelException(Exception):
pass
def get_voltage(channel):
if channel not in [0, 1]:
raise InvalidChannelException(f"Use channel 0 or 1, not {channel}")
...
return voltage
(ecpc) > get_voltage.py
Traceback (most recent call last):
File "get_voltage.py", line 1, in < module >
get_voltage(2)
File "exception_channel.py", line 6, in get_voltage
raise InvalidChannelException(f"Use channel 0 or 1, not {channel}")
InvalidChannelException: Use channel 0 or 1, not 2
Je kunt op deze manier voorkomen dat iemand dagen kwijt is aan het overdoen van achteraf verkeerd gebleken metingen. Ook kun je 'vage' foutmeldingen omzetten in duidelijkere foutmeldingen:
class NoCurrentError(Exception):
pass
def R(U, I):
try:
R = U / I
except ZeroDivisionError:
raise NoCurrentError("There is no current flowing through the resistor.")
return R
ZeroDivisionError
krijg je nu een NoCurrentError
. Je programma crasht nog steeds (wellicht niet handig) maar de foutmelding is nu wel specifiek voor het probleem en kan in de rest van je programma wellicht beter afgevangen en opgelost worden. Misschien beter dan niet crashen en een mogelijk foute waarde doorgeven. Die afweging zul je zelf moeten maken.
exceptions
De volgende code berekent een gemiddelde van een lijst getallen:
Er is alleen geen foutafhandeling en dat kan leiden tot exceptions. De volgende aanroepen zorgen voor een crash (probeer ze allemaal uit!): Pas de functieaverage()
zodanig aan dat bij bovenstaande aanroepen slechts een waarschuwing wordt geprint. Vang daartoe de exceptions netjes af en geef de waarde None
terug wanneer een gemiddelde niet berekend kan worden. Dus bovenstaande drie aanroepen krijgen None
terug terwijl er een waarschuwing wordt geprint.
Uitwerkingen
def average(values):
try:
average = sum(values) / len(values)
except TypeError:
average = None
print("Input is not correct type")
except ZeroDivisionError:
average = None
print("Input is empty")
return average
print(average([1, 2, 3]))
average([])
# # gives: ZeroDivisionError: division by zero
average(4)
# # gives: TypeError: 'int' object is not iterable
a = average("12345")
# # gives: TypeError: unsupported operand type(s) for +: 'int' and 'str'
# print(a)
(ecpc) > python exceptions.py
2.0
Input is empty
Input is not the correct type
Input is not the correct type
-
We gaan ervan uit dat iedereen bekend is met recente versies van Python en we gaan niet in op de -- soms ingrijpende -- veranderingen die de taal heeft ondergaan. Python 2 is dood. Leve Python 3! ↩
-
Tenzij je al veel zelf hebt geprogrammeerd in Python, buiten de cursussen om. ↩
-
De programmertaal C ligt dichter bij machinetaal dan Python en is daarmee veel sneller maar ook veel minder geavanceerd. ↩
-
Echt. De sinus van 2000 $x$-waardes berekenen kostte NumPy in een test 11.6$\micro$s en de for-loop wel 1357.7$\micro$s. ↩
-
Strikt genomen is dit niet helemaal waar. Je kunt een nieuwe array creëren door meerdere arrays aan elkaar te plakken. Maar een eenvoudige
append()
-method bestaat niet voor arrays. ↩ -
Letterlijk: onveranderbaar. ↩
-
Daar is bijvoorbeeld de
collections.namedtuple()
dan weer handig voor. ↩ -
Notatie hetzelfde, maar gebruik nu
{
}-haakjes. ↩ -
Dunder staat voor double underscore, de twee lage streepjes die om de naam heen staan. ↩
-
Maar dat is niet verplicht, je mag in principe zelf een naam kiezen. Doe dat echter niet. ↩
-
Absolute waarde of beter, norm, van een vector is eenvoudig gezegd haar lengte. ↩
-
Het verschil tussen de twee is subtiel. De Pythondocumentatie geeft aan dat de
__repr__
altijd ondubbelzinnig moet zijn, terwijl de__str__
vooral leesbaar moet zijn. Voor eenvoudige objecten zijn ze veelal gelijk. ↩ -
Calmcode doet een goeie poging om dit rustig uit te leggen, kijk daarvoor op https://calmcode.io/decorators/functions.html ↩
-
DRY staat voor Don't Repeat Yourself, een belangrijk principe in software engineering. ↩
-
Dat bestand is vaak leeg, maar kan code bevatten die gerund wordt zodra het package wordt geïmporteerd. ↩
-
En wat mij betreft: een fout dat zoiets überhaupt kan in Python. Zen of Python: explicit is better than implicit. ↩
-
Python Software Foundation. Python package index. URL: https://pypi.org. ↩↩
-
Ivo van Vulpen and Martijn Stegeman. Wetenschappelijk programmeren. 2020. URL: https://progns.proglab.nl/syllabus. ↩
-
Anthony Scopatz and Kathryn D. Huff. Effective Computation in Physics. O'Reilly Media, Inc., 2015. URL: https://www.oreilly.com/library/view/effective-computation-in/9781491901564/. ↩
-
Allen Downey. Think Python. Green Tea Press, 2nd edition edition, 2015. URL: https://greenteapress.com/wp/think-python-2e/. ↩
-
Tim Peters. Zen of python. URL: https://groups.google.com/d/msg/comp.lang.python/B_VxeTBClM0/L8W9KlsiriUJ. ↩
-
Chaitanya Baweja. Contemplating the zen of python. URL: https://medium.com/better-programming/contemplating-the-zen-of-python-186722b833e5. ↩
-
Python Software Foundation. The python standard library. URL: https://docs.python.org/3/library/. ↩↩
-
Real Python. Real python: python tutorials. URL: https://realpython.com. ↩
-
Malay Agarwal. Operator and function overloading in custom python classes. URL: https://realpython.com/operator-function-overloading/ (visited on 2020-06-25). ↩
-
Geir Arne Hjelle. Primer on python decorators. 2018. URL: https://realpython.com/primer-on-python-decorators/. ↩