Runde 1a: grundlagende Datentypen und Datenstrukturen#

Diese Seite: Auswahl, Besprechung und minimale Vertiefung von ausgewählten Sprachelementen für Runde 1a: grundlagende Datentypen und Datenstrukturen, hier: Python native datatypes.

Zugehöriges Quizz: Python Datentypen (R1): Quizz

immutables vs. mutables#

Bei Zahlen verhalten sich x und y, wie man es auch aus anderen Programmiersprachen kennt:

x = 1
y = x
print("y vorher ", y)
y vorher  1
x is y
True
x = x + 1000
print("y nachher", y)
y nachher 1
x == y
False

Bei Listen verhalten sich x und y wie Pointer:

x = [1, 2, 3]
y = x
print("y vorher ", y)
y vorher  [1, 2, 3]
x == y
True
x is y
True
x += [ 1000 ] # append 4 to the list pointed to by x
print("y nachher", y)
y nachher [1, 2, 3, 1000]
x == y
True

VORSICHT: Manche Listen-Operationen arbeiten in place. Andere erzeugen eine Kopie der ursprünglichen Liste: siehe unten!

Typ eines Objektes#

x = 4
type(x)
int

Als Typ der Zahl 4 wird int ausgeben. intist ein eingebautes Python-Objekt. Alle Objekte haben einen bestimmten Typ. Ob auch int einen Typ hat?

type(int)
type

Fun fact, für uns irrelevant: Irgendwann wird es zirkulär:

type(type)
type

Python scalar types (siehe 05: Built-In Types: Simple Values): int, float, complex, bool, str, NoneType

Vergleichsoperatoren#

Auch Typen kann man abfragen. Das Ergebnis einer Abfrage ist vom Typ bool.

pi = 3.1415
type(type(pi) == float)
bool

Es kann natürlich auch einer Variablen zugewiesen werden:

is_float = type(pi) == float
is_float
True

Ob eine Variable – hier pi – einen von mehreren möglichen Typen hat, fragen wir mit isinstance() ab. Im Gegensatz zu type() werden hier auch Subklassen berücksichtigt.

isinstance(pi, (float, int))
True

Auch list, tuple, dict sind Typen:

isinstance([1,2,3], list)
True
isinstance({1:"eins",2:"zwei",3:"drei"}, dict)
True

Python Scalar Types und arithmetische Operatoren#

In Whirlwind wird a + b etc. zunächst anhand von simple types eingeführt – trivial.

Aber was passiert, wenn man in a + b auf andere Typen anwendet? Gar die Typen mischt? Jetzt wird es interessant! Einiges geht ganz intuitiv. Anderes, das nicht geht, haben wir auskommentiert.

#Addition von Strings
a = "Hallo"
b = "Welt!"
a + b
'HalloWelt!'
# Multiplikation von String und Integer
a + "." * 3 + b
'Hallo...Welt!'
# Addition von Listen:
a = [1, 3, 5]
b = [2, 4, 6]
c = a + ["Hallo Welt!"] + b
c
[1, 3, 5, 'Hallo Welt!', 2, 4, 6]
# Multiplikation von Liste und Integer
a * 2
[1, 3, 5, 1, 3, 5]
# das geht nicht, wirft einen Fehler
# a * b

Listen#

  • Whirlwind:

L = [2, 3, 5, 7]

Zu einer Liste kann man auf verschiedene Art und Weise Elemente hinzufügen.

Append: Ein einzelnes Element hinzufügen.

L.append(1000)
L
[2, 3, 5, 7, 1000]

Extend: Eine andere Liste anfügen. Oder einfach so (augmented assignment operator):

L += [ 1001 ]
L
[2, 3, 5, 7, 1000, 1001]

Oder explizit mit einem extend():

L2 = [ "Otto", ["x", "y"] ]
L.extend(L2)
L
[2, 3, 5, 7, 1000, 1001, 'Otto', ['x', 'y']]

Bitte ausprobieren: Was passiert, wenn man in der vorhergehenden Anweisung statt extend() ein append() verwendet hätte?

Offensichtlich wird bei +=, bei append() und extend() direkt L verändert, in place. Das kann unerwünschte Nebeneffekte haben, und Denkfehler erzeugen. So geht es NICHT:

L = [ 1, 2, 3]
L_neu = L.append(1000)
type(L_neu)
NoneType

Die folgende Operation arbeitet nicht in place, sondern erzeugt eine Kopie:

L_neu = L + [1001]
L, L_neu
([1, 2, 3, 1000], [1, 2, 3, 1000, 1001])

Es lässt sich auch abfragen, ob der Typ eines Objekts eine Liste ist:

isinstance(L, (list, tuple)), isinstance(L, int), isinstance(L[0], int)
(True, False, True)

Tupel#

Tupel sind „Listen mit runden Klammern“. In Runde 1 können wir Tupel vereinfacht einführen als Listen, die immutable sind, und auch keine append-etc. Methoden haben. Typische Verwendungsweisen von Tupeln:

# Schreibweise mit oder ohne runde Klammern
a = 1, 2, "Hallo"
a
(1, 2, 'Hallo')
a = 1
b = 2
(a, b) = (b, a)
print(a, b)
2 1

Indexing und Slicing#

L = [0, 1, 2, 3]

Anders als bei Vektoren und Matritzen in der Mathematik beginnt in Python der Index mit 0.

Wir fragen ein einziges (nämlich das erste) Element einer Liste an. Das Ergebnis ist das erste Element.

result = L[0]
result, type(result)
(0, int)

Mit der Slicing-Syntax [:] oder [::] fragen wir eine Teil-Liste an. Das Ergebnis ist eine Liste.

result = L[0:2]
result, type(result)
([0, 1], list)

Für Anwendungen oder Nutzer, bei denen das erste Element nicht mit [0], sondern mit [1] angesprochen wird, kann es sinnvoll sein, alle “nullten” Elemente auf 0 zu setzen:

Einmaleins = [
    [0,0,0,0,0],  # Einmaleins[0] ist eine Liste!
    [0,1,2,3,4,5],
    [0,2,4,6,8,10],
    [0,3,6,9,12,15],
    [0,4,8,12,16,10],
    [0,5,10,15,20,25] ]
Einmaleins[2][3]
6

Slicing von Strings#

In mancher Hinsicht verhält sich auch ein String wie eine Liste. Insbesondere funktioniert Indexing und Slicing:

text = "Hallo Welt!"
text[6:10]
'Welt'
# jeder zweite Buchstabe im Alphabet
abc = "abcdefghijklmnopqrstuvwxyz"
abc[0::2]
'acegikmoqsuwy'

Wenn man zu einem String etwas hinzuaddiert, wird im Speicher ein neuer Spiecherplatz gesucht und das Ergebnis dorthin kopiert. (Das ist eine teure Operation, aber wir können sie durch ein List-join ersetzen, siehe unten).

abcZ = abc + "Z"
abcZ
'abcdefghijklmnopqrstuvwxyzZ'

eine schicke Anwendung: Palindrome sind Wörter, die sich vorwärts und rückwärts gleich lesen.

rp = "ReliefpfeileR"

a = rp[ 0 : len(rp) //2 ]    # die erste Hälfte des Strings
b = rp[ -len(rp) //2 +1 :  ] # die zweite Hälfte
br = b[::-1]     # die zweite Hälfte rückwärts

ist_Palindrom = a == br

print(f"{rp=}\n{a=}\n{b=}\n{br=}\n{ist_Palindrom=}")
rp='ReliefpfeileR'
a='Relief'
b='feileR'
br='Relief'
ist_Palindrom=True

Ein String ist immutable. Deswegen gibt es auch kein append(), das folgende geht nicht:

# abc[2] = "C"
#abc.append("!")

Unterschied String und Liste:

  • Wenn man zu einem String etwas hinzufügt, wird ein neues Objekt erzeugt;

  • fügt man zu einer Liste etwas hinzu, geschieht das in place.

So sieht das im Detail aus:

abc = ["a", "b", "c"]
a = abc

abc += [ "Z" ] # das mutable Objekt wird in place verlängert
b = abc

print(f"{a=}\n{b=}\n{a is b}")
a=['a', 'b', 'c', 'Z']
b=['a', 'b', 'c', 'Z']
True
abc = "abc"
a = abc

abc += "Z" # das immutable Objekt wird kopiert
b = abc

print(f"{a=}\n{b=}\n{a is b}")
a='abc'
b='abcZ'
False

Das Slicing von Strings und Listen ist sehr ähnlich. Ähnlich kann man durch einen String „durchlaufen“:

for c in "Hallo":
    print(c)
H
a
l
l
o

Und natürlich kann man einen String in eine Liste verwandeln:

hallo_liste = list("Hallo")
hallo_liste
['H', 'a', 'l', 'l', 'o']

Und eine Liste in einen String:

"".join(hallo_liste)
'Hallo'

Dicts#

numbers = { 'one':1, 'eins':1, 'two':2, 'zwei':2, 'three':3 }
# Access a value via the key
numbers['two']
2

Die Liste der Keys und die Liste der Values sind gleich lang:

numbers_keys = list(numbers.keys())
print(numbers_keys)

numbers_values = list(numbers.values())
print(numbers_values)
['one', 'eins', 'two', 'zwei', 'three']
[1, 1, 2, 2, 3]

Vorschau auf R2: Zwei Listen kann man auch wieder zu einem Dict zusammenfügen, wie einen Reißverschluss.

zusammengefuegt = zip(numbers_keys, numbers_values)
print(dict(zusammengefuegt))
{'one': 1, 'eins': 1, 'two': 2, 'zwei': 2, 'three': 3}

Sets#

Unkompliziert, wie in der Mengenlehre.

Funktionen#

Im Folgenden verschiedene Versionen einer einfache Berechnung: „Erhöhe einen (z.B. Lebensmittel-) Preis um die Inflationsrate”, hier als Funktion eingepackt.

So sollte es sein: Alle relevanten Informationen werden explizit als Parameter mitgegeben:

def inflation(x, Inflationsrate):
    """Gibt einen Warenpreis um die Inflation erhöht zurück."""
    
    Erhoehung = x * ( Inflationsrate / 100 )
    
    return round( x + Erhoehung, 2 )
Butter = 1
inflation(Butter, 30)
1.3

Sichtbarkeit von Variablen#

So kann man es hässlicher auch machen:

  • Die Funktion greift auf die globale Variable Inflationsrate zurück. Das ist hässlich, aber bisweilen üblich, und meist schlechter Programmierstil.

  • In der Funktion wird eine Variable verdeckter_Zuschlag definiert: Die Funktion berechnet etwas anderes, als sie angibt. Auch das ist in der Wirtschaft üblich.

  • Wir haben auf die Variablen verdeckter_Zuschlag und Erhoehung von außen keinen Zugriff, sie sind lokal.

Inflationsrate = 30  # Angabe in Prozent

def inflation_plus_zuschlag(x):
    """Gibt einen Warenpreis ungefähr um die Inflation erhöht zurück."""
    
    # Wir rechnen intern mit einer leicht erhöhten Inflationsrate
    verdeckter_Zuschlag = 0.10
    Erhoehung = x * ( Inflationsrate / 100 + verdeckter_Zuschlag )
    
    return round( x + Erhoehung, 2 )
Butter = 1.99
inflation_plus_zuschlag(Butter)
2.79
Inflationsrate
30
# das gibt einen Fehler
# verdeckter_Zuschlag
# Diese Variable ist von außen nicht zugänglich:
# print(verdeckter_Zuschlag)
a = 1
def zweimalaa():
    a = "Das ist ein Text"
    return 2 * a
zweimalaa()
'Das ist ein TextDas ist ein Text'
a
1

List Comprehension#

List Comprehensions sind syntactic sugar: Kaum mehr als eine verkürzte Schreibweise für das einfachste aller einfachen Muster für for-Schleifen. Aber dennoch um so eleganter!

Beispiel: Wir wollen eine Liste aller Quadrate der Zahlen zwischen 0 (inclusive) und 5 (exclusive) erzeugen.

Dazu kennen wir in Runde 1 bereits dieses typische Muster:

# leere Ergebisliste initialisieren
quadratzahlen_liste = []

# x durch eine Grundmenge durchlaufen lassen
for x in range(5): # nimmt nacheinander die Werte 0, 1, 2, 3, 4 an
    print(f"{x=}", end = ", ")
    
    # mit x etwas machen und an die Ergebnis-Liste anhängen
    quadratzahlen_liste.append( x*x )
    
# Das Ergebnis steht dann hier
quadratzahlen_liste
x=0, x=1, x=2, x=3, x=4, 
[0, 1, 4, 9, 16]

Diesr Code nochmal aufs Wesentliche reduziert:

quadratzahlen_liste = []
for x in range(5):
    quadratzahlen_liste.append( x*x)
quadratzahlen_liste
[0, 1, 4, 9, 16]

Weil dieses Muster so überaus typisch ist und häufig vorkommt, gibt es dafür eine Kurzform:

quadratzahlen_liste = [  x*x for x in range(5) ]
quadratzahlen_liste
[0, 1, 4, 9, 16]
type(quadratzahlen_liste)
list
quadratzahlen_liste[2]
4

Als die Rechner noch langsam waren, hat man teure Rechnungen einmal vorab durchgeführt und dann tabellarisch angelegt – ein typischer Einsatzzweck von Dicts!

Natürlich lässt sich auch ein Dict Mit einer Comprehension erzeugen:

quadratzahlen_dict = { x: x*x for x in range(5) }
quadratzahlen_dict
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
type(quadratzahlen_dict)
dict
quadratzahlen_dict[2]
4

Nun kann man solch eine Liste oder solch ein Dict ähnlich einer Funktion verwenden.

Beispiel: Wie groß ist die Fläche eines Kreises mit 4 Meter Durchmesser?

pi = 3.1415
Radius = 2
Kreisfläche_aus_dict =  pi * quadratzahlen_dict[  Radius ]
Kreisfläche_aus_liste = pi * quadratzahlen_liste[ Radius ]
Kreisfläche_aus_dict, Kreisfläche_aus_liste
(12.566, 12.566)

List Comprehension, Vorausschau für Runde 2#

Eine List Comprehension geht natürlich auch mit verschachtelten Schleifen.

Beispiele: Das ganzkleine Buchstaben-Einmaleins geht so: a, aa, aaa, b, bb, bbb, ... ;-)

ergebnis = []
for Buchstabe in "abc":
    for Anzahl in range(1,4):
        ergebnis.append( Anzahl * Buchstabe )
ergebnis
['a', 'aa', 'aaa', 'b', 'bb', 'bbb', 'c', 'cc', 'ccc']

Ganz elegant mit einer List Comprehension als Einzeiler:

ergebnis = [ Anzahl * Buchstabe for Buchstabe in "abc" for Anzahl in range(1,4) ]
ergebnis
['a', 'aa', 'aaa', 'b', 'bb', 'bbb', 'c', 'cc', 'ccc']

Generische Erzeugung, bei der nur die zur Verwendung kommenden Buchstaben angegeben werden:

Alle_Buchstaben = "abcd"
kbe = [ Anzahl * Buchstabe for Buchstabe in Alle_Buchstaben for Anzahl in range(1,len(Alle_Buchstaben)+1) ]
print(kbe)
['a', 'aa', 'aaa', 'aaaa', 'b', 'bb', 'bbb', 'bbbb', 'c', 'cc', 'ccc', 'cccc', 'd', 'dd', 'ddd', 'dddd']

Comprehensiuon mit Ergebnis Tupel: Im Unterschied zur Liste wird nicht ein Tupel der Werte auf Vorrat anlegt, sondern es wird ein Generator-Objekt angelegt, das die benötigten Tupel erst bei Bedarf erzeugt (Details siehe 12: Generators).

ergebnis_tupel = ( x**2 for x in range(10000000) )
ergebnis_tupel
<generator object <genexpr> at 0x7028c5318ba0>

Schleifenvariable als Key verwenden#

pi = 3.14159
pi_string = "3.14159"

# Vorschau auf Runde 2:
# f-string, z.B. https://realpython.com/python-f-strings/
# pi_string = f"{pi}"

print(pi_string, type(pi_string))
3.14159 <class 'str'>

Das kennen wir schon: Durchlaufen durch eine Liste (hier die Ziffernfolge von pi):

for v in pi_string:
    print(v, end="_")
3_._1_4_1_5_9_
# Oder als Einzeiler (und das "join" als Vorschau aur Runde 2)
"_".join( [v for v in pi_string] )
'3_._1_4_1_5_9'

Die Schleifenvariable können wir nun zur Auswahl eines anderen Elementes verwenden:

  • Wir laufen mit einer Variable (z.B.) x direkt durch eine Liste durch,

  • und verwenden x dann als Index (Pointer, Key etc.), um in einer anderen Datenstruktur ein Objekt zu identifizieren.

Aussprache_BY = { "0": "null", "1": "oans", "2": "zwoa", "3": "drei", 
                 "4": "viea", "5": "fümpf", "6": "seggs", 
                 "7": "siam", "8": "acht", "9": "nein",
                ".": "Komma", "!": "gsuffa!"}
for v in pi_string: 
    print( Aussprache_BY[v], end=" " )
print(Aussprache_BY["!"])
drei Komma oans viea oans fümpf nein gsuffa!
# Oder als Einzeiler
ergebnis = [ Aussprache_BY[v] for v in pi_string + "!" ]
ergebnis
['drei', 'Komma', 'oans', 'viea', 'oans', 'fümpf', 'nein', 'gsuffa!']