Skip to content

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

  1. Open Visual Studio Code.
  2. Open de map ECPC).
  3. Maak een bestand zen-of-python.py met daarin de onderstaande code:
    import this
    
    ECPC
    ├── zen-of-python.py
    └── •••
  4. 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:

zen.py
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

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:

names = ['Alice', 'Bob', 'Carol']
i = 0
while i < len(names):
    print("Hi,", names[i])
    i = i + 1
en ook niet:
names = ['Alice', 'Bob', 'Carol']
for i in range(len(names)):
    print("Hi,", names[i])
waarbij je loopt over een index i. Gebruik liever het feit dat een lijst al een iterator is:
names = ['Alice', 'Bob', 'Carol']
for name in names:
    print("Hi,", name)
Deze code is bovendien veel korter en gebruikt minder variabelen.

Itereren op de python-manier

  1. Neem het onderstaande script over.
  2. Itereer over de lijst voltages op de python-manier.
  3. Print voor elk item in de lijst de waarde in mV. Bijvoorbeeld: "The voltage is set to 0 mV."
voltages = [0, 50, 100, 150, 200, 250, 300] #mV
Uitwerkingen

iterator.py
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

Enumerate

Soms is het nodig om de index te hebben, bijvoorbeeld wanneer je een namenlijstje wilt nummeren:

Terminal
1. Alice
2. Bob
3. Carol

Dit kan dan in Python-code het makkelijkst als volgt:

for idx, name in enumerate(names, 1):
    print(f"{idx}. {name}")
Hier maken we gebruik van de enumerate(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.

  1. 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)$.
  2. Print die rij onder elkaar (één getal per regel, met drie decimalen).
  3. Geef weer of het getal 3 voorkomt in die rij en geef weer of het getal 4 voorkomt in die rij.
Uitwerkingen

list.py
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

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:

import numpy as np
from numpy import pi

x = np.linspace(0, pi, 20)
y = np.sin(x)
NumPy voert de berekeningen uit binnen een C-bibliotheek3 en is daarmee veel sneller dan een berekening in Python zelf:

import math
x = [0.00, 1.05, 2.09, 3.14, 4.19, 5.24, 6.28]
y = []
for u in x:
    y.append(math.sin(u))
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:

import numpy as np
from numpy import pi

x = np.linspace(0, pi, 100)
y = np.sin(x)

np.array

Doe hetzelfde als de vorige opdracht met lists, maar nu met NumPy arrays:

  1. 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)$.
  2. Print die rij onder elkaar (één getal per regel, met drie decimalen).
  3. Geef weer of het getal 3 voorkomt in die rij en geef weer of het getal 4 voorkomt in die rij.
Uitwerkingen

np_array.py
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

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:

  1. Maak een dictionary constants met de waardes van de (natuur)constantes $\pi$, de valversnelling $g$, de lichtsnelheid $c$ en het elementaire ladingskwantum $e$.
  2. Print de namen -- niet de waardes -- van de constantes die zijn opgeslagen in constants.
  3. 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.
  4. Maak een dictionary measurement die de resultaten van een meting bevat: een spanning van 1.5 V bij een stroomsterkte van 75 mA.
  5. Bereken de weerstand van de schakeling op basis van de voorgaande meting en bewaar het resultaat in dezelfde dictionary.
Uitwerkingen

dictionaries.py
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

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, 0), (1, 0), (0, 1)]
Hier is 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:
coords = [{"x": 0, "y": 0}, {"x": 1, "y": 0}, {"x": 0, "y": 1}]
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:

(x, y, z) = (2, 3, 4)
Na deze operatie geldt $x = 2$, $y = 3$ en $z = 4$. Je mag zelfs de haakjes weglaten voor nog compactere notatie:
x, y, z = 2, 3, 4
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()
Het uit elkaar plukken van argumenten kan zelfs als je een functie aanroept:
def power(a, b):
    return a ** b


# regular function call
power(2, 7)

# function call with tuple unpacking
args = 2, 7
power(*args)
Wat zelfs werkt is dictionary unpacking. Je kunt aan functies ook argumenten bij naam meegeven -- de volgorde maakt dan niet uit en je maakt in je programma expliciet duidelijk welke argumenten je meegeeft. Dat werkt zo:
# 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:

Terminal
1 3 5 7 9
Je mag er niet vanuit gaan dat de lijst altijd 5 elementen bevat.

Uitwerkingen
odds = [1, 3, 5, 7, 9]

# What is the difference between printing with and without a star?
print(*odds)

print(odds)
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:

l = [1, 2, 2, 3, 5, 5]
set(l)
# {1, 2, 3, 5}
Je moet even oppassen: de {}-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:
is_set = {1, 2, 3, 4}
is_dict = {1: 1, 2: 4, 3: 9, 4: 16}
Dat gaat alleen mis als je een lege set wilt maken. Daarvoor zul je expliciet de set()-constructor moeten gebruiken:
is_dict = {}
is_set = set()
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:

from math import sin
x = [0.00, 1.05, 2.09, 3.14, 4.19, 5.24, 6.28]
y = [sin(u) for u in x]
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:
import numpy as np
x = [0.00, 1.05, 2.09, 3.14, 4.19, 5.24, 6.28]
x = np.array(x)
y = np.sin(x)

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:

pdf.py
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

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:

  1. Maak een lijst van de getallen 1 tot en met 10.
  2. Gebruik een 'gewone' for-loop om een lijst te maken van de derdemachtswortel van de getallen.
  3. Maak nogmaals een lijst van de derdemachtswortel van de getallen maar gebruik nu list comprehension.
  4. Gebruik tot slot arrays om de lijst met derdemachtswortels van de getallen te maken.
Uitwerkingen

for_loop.py
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

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:

a = ["kiwi", "banana", "apple"]
print(sorted(a))
(ecpc) > python sort.py 

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:
a = ["kiwi", "banana", "apple"]

print(len("apple"))
print(sorted(a, key=len))
(ecpc) > python length.py 

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 

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 

Aangezien de definitie van een lambdafunctie zelf ook een expression is kun je het sorteren op de tweede letter zelfs in één regel doen:
a = ["kiwi", "banana", "apple"]

print(sorted(a, key=lambda value: value[1]))
(ecpc) > python one_line.py 

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)
Het is hierbij wel belangrijk dat 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:

f = models['linear']
f(5, a=2, b=3)
# 13
Maak een grafiek van de sinusfunctie op het domein $[0,\, 2\pi]$ met parameters $a=1$, $b=2$, $c=2$ en $d=\frac{\pi}{2}$.

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 

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
Lekker kort, want we hoeven geen lijst bij te houden! Als je de functie aanroept krijg je geen resultaat terug, maar een generator. Als je de waardes wil zien dan gebruik je 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
Als de generator is uitgeput (de for-loop is afgelopen, de functie sluit af) dan geeft Python een 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:
for square in calculate_squares_up_to(5):
    print("Still calculating...")
    print(square)
(ecpc) > python squares.py 

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

collatz.py
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

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
De speciale __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:

Terminal
>>> 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>
In de eerste regels maken we twee vectoren v_1 en v_2 en berekenen de lengtes11 ||v_1||, ||v_2|| en ||v_1 + v_2||. Ook kunnen we de coördinaten van de som bekijken. Het gaat mis als we de somvector willen printen of willen kijken wat voor object het is. We krijgen technisch juiste, maar totaal onbruikbare informatie terug. Dit lossen we op met het definiëren van __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)
Terminal
>>> v1 + v2
Vector: (1, 1)
>>> print(v1 + v2)
Vector: (1, 1)
We raden je aan altijd een zinnige __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
Het werkt, maar we moeten er wel steeds aan denken om 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:
def squared_func(func):
    def inner_func(a, b):
        return func(a, b) ** 2

    return inner_func


g = squared_func(f)
g(3, 4)
# 144

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:

def squared_func(func):
    def inner_func(*args):
        return func(*args) ** 2

    return inner_func
En nu komt het: in Python kun je de decorator syntax gebruiken om je functie te vervangen door een iets aangepaste functie. In plaats van:
f = squared_func(f)
op te nemen in je code kun je de functie meteen `decoraten' als volgt:
@squared_func
def f(a, b):
    return a * b

f(3, 4)
# 144

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

decorators.py.py
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

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:

math.py
import math
print(math.sqrt(2))
print(math.pi)
print(math.sin(.5 * math.pi))
(ecpc) > python math.py

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:

square.py
def square(x):
    return x**2


print(f"The square of 4 is {square(4)}")
(ecpc) > python square.py

De uitvoer is zoals verwacht. Maar nu willen we in een nieuw script, count_count.py, de functie importeren en gebruiken:

count_count.py
import square

print(f"The square of 5 is {square.square(5)}")
(ecpc) > python count_count.py

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:

  1. Alle 'extra' code verwijderen uit de module (square.py)
  2. 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:

square.py
def square(x):
    return x**2


if __name__ == "__main__":
    print(f"The square of 4 is {square(4)}")
Wanneer je een python script runt is de speciale variabele __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 

(ecpc) > python count_count.py 

Het if __name__ == '__main__'-statement wordt heel veel gebruikt in Python modules.

modules

  1. Maak zelf de bestanden square.py en just_count.py aan.
    ECPC
    ├── square.py
    ├── just_count.py
    └── •••
  2. Run just_count.py zonder het if __name__ == '__main__'-statement.
  3. Run just_count.py met het if __name__ == '__main__'-statement.
  4. Voeg print(f"{__name__ = }") toe bovenaan square.py.
  5. Run square.py en kijk wat __name__ is.
  6. 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:

import numpy.test
import scipy.test
In bovenstaande code zijn 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):

script.py
# 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.

  1. Maak in de map ECPC een package models met twee modules: polynomials.py en tests.py.
  2. In de polynomials-module maak je een functie line(x, a, b) die de vergelijking voor een lijn voor ons berekent: $y = ax + b$.
  3. In de tests-module maak je een functie test_line() die het volgende doet:

    1. gebruik de line()-functie uit de polynomials-module om de $y$-waarde uit te rekenen voor een bepaald punt bij een gegeven $a$ en $b$.
    2. Vergelijk die berekende waarde met de waarde die het volgens jou moet zijn (met de hand nagerekend).
    3. Print TEST PASSED als het klopt, en TEST FAILED als het niet klopt.
  4. Maak een bestand practice-packages.py die:

    1. Een grafiek maakt van jouw lijn. Bepaal zelf het domein en de waardes voor $a$ en $b$.
    2. De test uitvoert door de test_line()-functie aan te roepen.
    3. Pas je line()-functie eventjes aan om te kijken of je test ook echt werkt. Bijvoorbeeld: bij $y = ax$ zou je TEST FAILED moeten zien.
Uitwerkingen

De mappen structuur ziet er als volgt uit:

└── ECPC
           ├── •••
           ├── practice-packages.py
           └── models
                      ├── __init__.py
                      ├── polynomials.py
                      └── tests.py

polynomials.py
def line(x, a, b):
    y = a * x * b
    return y
tests.py
from models.polynomials import line


def test_line():
    actual = line(2, 4, 3)
    expected = 11

    if actual == expected:
        print("TEST PASSED")
    else:
        print("TEST FAILED")
practice-packages.py
import numpy as np
from models.polynomials import line
from models import tests
import matplotlib.pyplot as plt

x = np.arange(0, 28)
a = 1
b = 7

plt.plot(x, line(x, a, b))
plt.show()

tests.test_line()

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:

# module1.py

# relative imports
from . import module2
from ..package2 import module3

# absolute imports
from my_package.package1 import module2
from my_package.package2 import module3
Absolute imports zijn wat meer werk, maar je maakt wel heel duidelijk welke module je wilt importeren. Relative imports zorgen in de praktijk regelmatig voor -- soms lastig te vinden -- bugs. Als je tegen problemen aanloopt: gebruik dan absolute imports.

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

  1. Zoek de The Python Standard Library lijst op in bijvoorbeeld de Python documentatie.
  2. Welke bibliotheken heb je al eerder gebruik van gemaakt?
  3. Kies een bibliotheek uit die jouw aandacht trekt en neus door de documentatie.
  4. 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.

PyPI

Later in de cursus leren jullie werken met Poetry daarmee is het gemakkelijk om je eigen project op PyPI te zetten. Andere studenten gingen jullie al voor:

  1. Ga naar pypi.org en zoek naar het project gammaspotter.

Exceptions

Exceptions zijn de foutmeldingen van Python. Je krijgt ze als je bijvoorbeeld probeert te delen door nul

divide.py
print(1/0)
(ecpc) > python divide.py

of wanneer je een typefout maakt:

particle.py
s = "particle"
s.upler()
(ecpc) > python particle.py

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.py
number_input = input("Give a number: ")
number_multiply = 2.8
print(number_input * number_multiply)
  1. Neem het script hierboven over en voer het uit.
  2. Wat voor soort error geeft Python terug?
  3. In welke regel van het script zit volgens de Traceback het probleem?
  4. 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 is number_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 je number_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:

def R(U, I):
    try:
        R = U / I
    except ZeroDivisionError:
        R = "Inf"
    return R
print(R(10, 2))
# 5.0
print(R(10, 0))
# Inf

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
Met deze uitvoer in het geval dat er iets mis gaat:
voltage = get_voltage(1)
print(voltage)
# 1.0
voltage = get_voltage(2)
print(voltage)
(ecpc) > get_voltage.py 

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
In plaats van een 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:

def average(values):
    return sum(values) / len(values)    
Er is alleen geen foutafhandeling en dat kan leiden tot exceptions. De volgende aanroepen zorgen voor een crash (probeer ze allemaal uit!):
average([])
average(4)
average("12345")
Pas de functie average() 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

exceptions.py
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


  1. 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! 

  2. Tenzij je al veel zelf hebt geprogrammeerd in Python, buiten de cursussen om. 

  3. De programmertaal C ligt dichter bij machinetaal dan Python en is daarmee veel sneller maar ook veel minder geavanceerd. 

  4. 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. 

  5. 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. 

  6. Letterlijk: onveranderbaar. 

  7. Daar is bijvoorbeeld de collections.namedtuple() dan weer handig voor. 

  8. Notatie hetzelfde, maar gebruik nu {}-haakjes. 

  9. Dunder staat voor double underscore, de twee lage streepjes die om de naam heen staan. 

  10. Maar dat is niet verplicht, je mag in principe zelf een naam kiezen. Doe dat echter niet. 

  11. Absolute waarde of beter, norm, van een vector is eenvoudig gezegd haar lengte. 

  12. 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. 

  13. Calmcode doet een goeie poging om dit rustig uit te leggen, kijk daarvoor op https://calmcode.io/decorators/functions.html 

  14. DRY staat voor Don't Repeat Yourself, een belangrijk principe in software engineering. 

  15. Dat bestand is vaak leeg, maar kan code bevatten die gerund wordt zodra het package wordt geïmporteerd. 

  16. En wat mij betreft: een fout dat zoiets überhaupt kan in Python. Zen of Python: explicit is better than implicit. 

  17. Python Software Foundation. Python package index. URL: https://pypi.org

  18. Ivo van Vulpen and Martijn Stegeman. Wetenschappelijk programmeren. 2020. URL: https://progns.proglab.nl/syllabus

  19. 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/

  20. Allen Downey. Think Python. Green Tea Press, 2nd edition edition, 2015. URL: https://greenteapress.com/wp/think-python-2e/

  21. Tim Peters. Zen of python. URL: https://groups.google.com/d/msg/comp.lang.python/B_VxeTBClM0/L8W9KlsiriUJ

  22. Chaitanya Baweja. Contemplating the zen of python. URL: https://medium.com/better-programming/contemplating-the-zen-of-python-186722b833e5

  23. Python Software Foundation. The python standard library. URL: https://docs.python.org/3/library/

  24. Real Python. Real python: python tutorials. URL: https://realpython.com

  25. Malay Agarwal. Operator and function overloading in custom python classes. URL: https://realpython.com/operator-function-overloading/ (visited on 2020-06-25). 

  26. Geir Arne Hjelle. Primer on python decorators. 2018. URL: https://realpython.com/primer-on-python-decorators/