Crash-Kurs Python 3

Eine Einführung an Hand von Beispielen

https://www-user.tu-chemnitz.de/~hot/PYTHON/crash_kurs_py3/


Index

  1. Charakterisierung von Python
  2. Python-Umgebung starten und nutzen
  3. Code-Beispiele des Crash-Kurses
  4. Sammlung von Code-Beispielen

1. Charakterisierung von Python


2. Python-Umgebung starten und nutzen


3. Code-Beispiele des Crash-Kurses

Index:

  1. Learn python3 in Y minutes
  2. Python Cheat Sheets
  3. A Whirlwind Tour of Python
  4. Examples von python.org
  5. Nutzung des Moduls requests
  6. Simpler Web-Server mit Python
  7. Messwerte aus Textdatei einlesen und als CSV-Daten ausgeben
  8. Diagramm mit matplotlib zeichnen
  9. Diagramm mit matplotlib in einem Jupyter Notebook zeichnen
  10. Messwert-Datei auswerten und grafisch darstellen
  11. Textdatei mit Zahlen auswerten
  12. Pretty Printer für JSON
  13. Nutzung von Paketen für SMTP-Mail
  14. Definition und Nutzung eines eigenen Moduls
  15. Kontrollstrukturen
  16. Strings
  17. Kodierung und Ausgabe von Strings
  18. Rechnen mit Python
  19. None und Boolean
  20. Tupel
  21. Listen
  22. Dictionaries
  23. Invertierung eines Dictionarys
  24. Mengen
  25. Funktionen
  26. Start-Beispiel OOP
  27. Minimalistische Klasse als Daten-Container
  28. spezielle Sichtbarkeitsregeln im Klassen-Körper
  29. Klasse mit Property und speziellen Methoden
  30. Gebundene und ungebundene Methoden
  31. Nutzung eines Overriding Descriptors

1. Learn python3 in Y minutes

2. Python Cheat Sheets

3. A Whirlwind Tour of Python

4. Examples von python.org

# 5 kleine Beispiele von https://www.python.org/
#
# leicht angepasst und ergänzt
#
# 13.11.2018

# -----------------------------------

# print() kann im interaktiven Modus entfallen

# Python 3: Simple arithmetic
print(1 / 2)
# 0.5

print(2 ** 3)
# 8

print(17 / 3)  # classic division returns a float
# 5.666666666666667

print(17 // 3) # floor division
# 5

# -----------------------------------

# Python 3: Simple output (with Unicode)
print("Hello, I'm Python!")
# Hello, I'm Python!

# Input, assignment
name = input('What is your name?\n')
print('Hi, %s.' % name)
# What is your name?
# Python
# Hi, Python.

# mit format() statt %
print('Hi, {}.'.format(name))

# mit f-string (verfügbar ab Python 3.6)
print(f'Hi, {name}.')
print(f'Hi, {name[:2].upper()}.')  # Nutzung eines Python-Ausdrucks
# Hi, PY.

# -----------------------------------

# For loop on a list
numbers = [2, 4, 6, 8]
product = 1
for number in numbers:
    # product = product * number
    product *= number
print('The product is:', product)
# The product is: 384

# -----------------------------------

# Python 3: List comprehensions
fruits = ['Banana', 'Apple', 'Lime']
loud_fruits = [fruit.upper() for fruit in fruits]
print(loud_fruits)
# ['BANANA', 'APPLE', 'LIME']

# List and the enumerate function
list(enumerate(fruits))
# [(0, 'Banana'), (1, 'Apple'), (2, 'Lime')]

# alternativ mit Generatorausdruck statt List comprehension
loud_fruits = (fruit.upper() for fruit in fruits)
print(list(loud_fruits))

# -----------------------------------

# Python 3: Fibonacci series up to n

# https://de.wikipedia.org/wiki/Fibonacci-Folge
#
# Die Fibonacci-Folge ist die unendliche Folge von natürlichen Zahlen, die
# (ursprünglich) mit zweimal der Zahl 1 beginnt oder (häufig, in moderner
# Schreibweise) zusätzlich mit einer führenden Zahl 0 versehen ist.[1] Im
# Anschluss ergibt jeweils die Summe zweier aufeinanderfolgender Zahlen die
# unmittelbar danach folgende Zahl:
#
# 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, …

def fib(n):
    a, b = 0, 1
    while a < n:
        print(a, end = ' ')
        a, b = b, a + b
        # statt tuple packing und sequence unpacking könnte man eine
        # Hilfsvariable c nutzen:
        # c = a + b ; a = b ; b = c
    print()

fib(1000)
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

# Variante 2 mit einer Liste aller Fibonacci-Zahlen als Ergebnis
def fib_list(n):
    a, b = 0, 1
    fib_numbers = [] # Start mit leerer Liste
    while a < n:
        fib_numbers.append(a) # a an Liste anhängen
        a, b = b, a + b
    return fib_numbers

# Ausgabe als Python-Liste
print(fib_list(1000))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

5. Nutzung des Moduls requests

# Nutzung des Moduls requests

# https://docs.python-requests.org/en/latest/

import requests

r = requests.get('https://www.tu-chemnitz.de')

print(r.status_code)
print(r.headers['content-type'])
print(r.encoding)
print(r.text[:100])

6. Simpler Web-Server mit Python

# simpler Web-Server mit Python 3

from http.server import HTTPServer, CGIHTTPRequestHandler
import os

# zur document root wechseln
os.chdir('/tmp')

# den CGIHTTP-Server auf Port 8080 starten
server = HTTPServer(('', 8080), CGIHTTPRequestHandler)
server.serve_forever()

CGI-Test-Skript cgi_test (Shell-Version):

#!/bin/sh

# CGI-Test-Skript, das vom CGIHTTPRequestHandler gerufen wird

# Ablage unter /tmp/cgi-bin
# chmod +x cgi_test
#
# /bin/python3 webserver.py
# curl localhost:8080/cgi-bin/cgi_test

echo 'Content-Type: text/plain; charset=UTF-8'
echo
echo 'Hallo'

CGI-Test-Skript cgi_test.py (Python-Version):

#!/bin/python3

# CGI-Test-Skript, das vom CGIHTTPRequestHandler gerufen wird

# Ablage unter /tmp/cgi-bin
# chmod +x cgi_test.py
#
# /bin/python3 webserver.py
# curl localhost:8080/cgi-bin/cgi_test.py

print('''Content-Type: text/plain; charset=UTF-8

Hallo''')

7. Messwerte aus Textdatei einlesen und als CSV-Daten ausgeben

# Messwerte aus einer Text-Datei einlesen und als CSV-Datei ausgeben

# CSV-Datei zum Schreiben öffnen
out = open('text.csv', 'w')

# Messwerte zeilenweise einlesen
for line in open('text'):
   # Whitespace am Anfang und Ende der Zeile entfernen
   line = line.strip()

   # Zeilen, die mit # beginnen, als Kommentare übergehen
   if line.startswith('#'):
       continue

   # die Zeile in eine Liste von Feldern aufspalten
   felder = line.split()

   # die Felder ausgeben
   print(felder)

   # die CSV-Darstellung erzeugen und in die CSV-Datei ausgeben; normalerweise
   # nimmt man hierfür einen CSV-Writer aus dem Standard-Modul csv, wir nutzen
   # hier ein simples join()
   print(','.join(felder), file=out)

8. Diagramm mit matplotlib zeichnen

# ein Diagramm mit matplotlib zeichnen

# plotting framework importieren
#   python3 -m venv MYVENV
#   source MYVENV/bin/activate
#   pip install matplotlib
import matplotlib.pyplot as plt

x = [1, 3, 5, 7, 9]
y = [17, 23, 16, 45, 9]
plt.plot(x, y)
plt.title('Ein duftes Diagramm')
plt.show()

9. Diagramm mit matplotlib in einem Jupyter Notebook zeichnen

# in einem Jupyter Notebook ein Diagramm mit matplotlib zeichnen

%matplotlib inline

import matplotlib.pyplot as plt

x = [1, 3, 5, 7, 9]
y = [17, 23, 16, 45, 9]
plt.plot(x, y)
plt.title('Ein duftes Diagramm')
plt.show()

10. Messwert-Datei auswerten und grafisch darstellen

# Auswertung und grafische Darstellung von Messwerten

# plotting framework importieren
import matplotlib.pyplot as plt

# Anzahl und Summe der relevanten Messwerte nullen
anzahl = summe = 0

# die X- und Y-Koordinaten für das Diagramm
x = []
y = []

# die Messwerte zeilenweise einlesen und verarbeiten
#
#   2018-04-06 17:17:03.224234 05 05.96 00.00 53.46
#
for zeile in open('leftdrill3.txt'):
    # die Zeile in Felder zerlegen
    felder = zeile.split()

    # das Datum aus Spalte 1 und die Leistung aus der letzten Spalte entnehmen;
    # die Leistung wird nach float konvertiert
    datum = felder[0]
    leistung = float(felder[-1])

    # das Datum anders formatieren: YYYY-MM-DD ==> TT.MM.JJJJ
    j, m, t = datum.split('-')
    datum = f'{t}.{m}.{j}'
    # oder:
    # datum = '{}.{}.{}'.format(t, m, j)
    # datum = t + '.' + m + '.' + j

    # nur Werte betrachten, die zwischen 50 und 60 liegen
    if 50 < leistung < 60:
        # Ausgabe von Datum und Leistung
        print(f'{datum} {leistung:.2f}')
        #print('{} {:.2f}'.format(datum, leistung))

        # die Leistung summieren und die Anzahl der Summanden inkrementieren,
        # so dass wir am Ende ein arithmetisches Mittel bilden können
        summe += leistung
        anzahl += 1

        # die aktuelle Anzahl als X-Koordinate und die Leistung als
        # Y-Koordinate vermerken
        x.append(anzahl)
        y.append(leistung)

if anzahl:
    # die Anzahl ist nicht 0, dann das arithmetische Mittel bilden
    print('Mittel', summe / anzahl)

# die ersten 15 Elemente der Listen x und y ausgeben
print(x[:15])
print(y[:15])

# das Diagramm zeichnen
plt.plot(x, y)
plt.title('Messwert-Diagramm')
plt.show()

11. Textdatei mit Zahlen auswerten

# importiere die Funktion reduce() aus dem Modul functools
from functools import reduce

# ein Wort (Zeichenkette) in eine Zahl vom Typ int, float oder complex
# konvertieren
def konvertiere(wort):
    zahl = None

    # der Reihe nach die Konvertierungsfunktionen probieren
    for funktion in int, float, complex:
        try:
            zahl = funktion(wort)
            # bei Erfolg verlassen wir die Schleife hier
            break
        except:
            # eine Ausnahme ignorieren
            pass

    # die ermittelte Zahl zurückgeben
    return zahl

# das Produkt der Zahlen einer Zeile bilden
def produkt(zeile):
    produkt = None

    # die Wörter der Zeile durchlaufen
    for wort in zeile.split():
        # das Wort in eine Zahl konvertieren
        zahl = konvertiere(wort)

        if zahl is None:
            # die Konvertierung schlug fehl
            print(f'{wort} wurde wegen Syntaxfehler ignoriert')
        else:
            # die erste Zahl einer Zeile als Produkt übernehmen und ab der 2.
            # Zahl diese mit dem bisherigen Produkt multiplizieren
            produkt = zahl if produkt is None else produkt * zahl

    # Rückgabe des Produkts bzw. von 0, wenn wir kein Produkt bilden konnten
    return 0 if produkt is None else produkt
    
# andere Implementierung der Produktbildung
def produkt2(zeile):
    # verschachtelte List Comprehension, die alle Wörter in Zahlen oder None
    # konvertiert und die None-Einträge entfernt
    zahlen = [zahl for zahl in [konvertiere(wort) for wort in zeile.split()] if zahl is not None]

    # mit reduce() und einer anonymen Lambda-Funktion die Elemente schrittweise
    # multiplizieren
    return reduce(lambda x, y: x * y, zahlen) if zahlen else 0

# MAIN

# die Daten als mehrzeiligen String definieren
daten = '''# ein Kommentar
2
3 4
aaa
2.5 3
2+3j
STOP
1000
'''

# die Daten in eine Datei schreiben; with sorgt für das Schließen der Datei
with open('daten.txt', 'w') as f:
    f.write(daten)

# Liste der gebildeten Produkte
produkte = []

# die Summe der Produkte
summe = 0

# die Datei zeilenweise auswerten (hier ohne with)
for zeile in open('daten.txt'):
    zeile = zeile.strip()

    if not zeile or zeile.startswith('#'):
        # Leer- und Kommentarzeilen übergehen
        continue

    if zeile == 'STOP':
        # Abbruch bei einer STOPP-Zeile
        break
    
    # das Produkt nach dem ersten Verfahren bilden und summieren
    summe += produkt(zeile)

    # das Produkt nach dem zweiten Verfahren bilden und an die Liste der
    # Produkte anhängen
    produkte.append(produkt2(zeile))

# die erste Summe ausgeben
print('Summe  =', summe)

# die Produktliste ausgeben, summieren und die Summe ausgeben
print('Produkte =', produkte)
print('Summe2 =', sum(produkte))

12. Pretty Printer für JSON

#!/bin/env python3
#
# JSON-File "schön" formatiert ausgeben
#
# 7.11.2023

import sys
import json

# das zu lesende JSON-File bestimmen
if 0:
    # Variante 1: mit vollständiger Alternative
    if len(sys.argv) == 1:
        # dem vorliegenden Skript wurden keine Kommandozeilenargumente übergeben
        json_file = 'services.json'
    else:
        # wir haben mindestens 1 Kommandozeilenargument und nutzen es als Name der
        # JSON-Datei
        json_file = sys.argv[1]

else:
    # Variante 2: mit ternärem Operator
    json_file = 'services.json' if len(sys.argv) == 1 else sys.argv[1]

# die JSON-Daten in ein Dictionary einlesen und schön formatiert wieder ausgeben
print(json.dumps(json.load(open(json_file)), indent = 2))

13. Nutzung von Paketen für SMTP-Mail

#!/bin/python3
#
# Mail-Versand mit dem Standard-Modul smtplib
#
# 15.5.2023

# Module smtplib und sys importieren
import smtplib
import sys

# MIMEText aus dem Modul text des Sub-Pakets email.mime des Pakets email
# importieren
from email.mime.text import MIMEText

# unser Mailtext
mail_text = '''
Hallo allerseits,

das ist eine kleine Mail mit äöü ÄÖÜ ß €.
'''

# eine MIMEText-Nachricht erstellen
msg = MIMEText(mail_text)

# Header setzen
msg['Subject'] = 'Mail von Python'
me = msg['From'] = 'otto@hrz.tu-chemnitz.de'
you = msg['To'] = 'hot@hrz.tu-chemnitz.de'

# Mail senden
s = smtplib.SMTP()
if len(sys.argv) > 1 and sys.argv[1] == 'd':
    # Kommandozeilenargument 1 lautet "d", daher Debug einschalten
    s.set_debuglevel(1)
#s.connect(host = 'mail.tu-chemnitz.de')
s.connect()
s.sendmail(me, [you], msg.as_string())
s.close()

14. Definition und Nutzung eines eigenen Moduls

#!/bin/env python3
# -*- coding: utf8 -*-

"""
Hello World: als Modul oder Programm nutzbar
"""

# Definition der parameterlosen Funktion "hello"
def hello():
    """Funktion hello(): Ausgabe des Strings 'hello world'"""
    print('hello world')

if __name__ == '__main__':
    # die Funktion "hello" ausführen, da der vorliegende Code als Programm
    # gestartet und nicht als Modul importiert wurde
    hello()

Nutzung des Moduls helloworld_nutzung.py:

#!/bin/env python3
# -*- coding: utf8 -*-

import helloworld

# die Funktion "hello" des Moduls "helloworld" aufrufen
helloworld.hello()

# den DOC-String von "hello" ausgeben
print(helloworld.hello.__doc__)

# Funktion help nutzen
help(helloworld)

15. Kontrollstrukturen

# Python-Kontrollstrukturen

import sys
import random
import time

# if / elif / else - vollständige und unvollständige Alternativen

# Hinweis: Python kennt keine Anweisung für die Mehrfachauswahl (analog switch
# bei C, Java, PHP, ...).

for z in range(-3, 3):
    if z < 0:
        print(z, 'kleiner 0')
    elif z == 0:
        print(z, 'ist 0')
    else:
        print(z, 'größer 0')

for z in 0, 1:
    if z: print('z ungleich 0:', z)

print('===')

# Major- und Minor-Version von Python ermitteln
major, minor = sys.version_info[:2]
#major, minor = sys.version_info.major, sys.version_info.minor

# Python-Version testen
if major == 3 or major == 2 and not minor < 7:
    print('modern: Python 3 oder 2.7')

if major != 3:
    print('kein Python 3')

# Mehrfachzuweisung
a = b = c = 5

# unvollständige Alternative
if a == b == c:
    # verketteter Vergleich
    print('alle gleich')

# vollständige Alternative in Ausdrucksform (ternärer Operator)
print(('' if a == b == c else 'nicht ') + 'alle gleich')
print()

# ---------------------

# while-Schleife / break / continue

n = 15
while n:
    if n < 2:
        break
    n -= 1
    if n % 3 == 0: # oder: if not n % 3:
        # Rest der Division durch 3 == 0; n ist also ganzzahliges Vielfaches von 3;
        # dann die nächste Schleifeniteration einleiten
        continue
    print(n)

print()

# ---------------------

# for-Schleife / break / continue

n = 0
with open('/etc/services') as f:
    # with schließt die geöffnete Datei am Ende automatisch
    for line in f:
        # ohne with:
        # for line in open('/etc/services'):
        line = line.strip()
        if not line or line.startswith('#'):
            # Leer- und Kommentarzeilen übergehen
            continue
        n += 1
        if n > 4:
            # Schleifenabbruch
            break
        print(line)

print()

# den Zufallszahlengenerator initialisieren
random.seed()

# Stop-Flag als Zufallszahl generieren
stop = (random.randint(0, 5) if 0 else int(time.time())) % 2

for preis in range(1, 60, 5):
    if 25 <= preis <= 40:
        # verketteter Vergleich
        print(preis, 'OK')
    elif preis < 20 or preis > 50:
        print(preis, 'kein Kauf')
    else:
        print(preis, 'grenzwertig')
        if stop:
            break

else:
    print('Schleife normal beendet')

print()

# ---------------------

# try / except - Ausnahmebehandlung (Exception Handling)

try:
    print(1 / 0)
except:
    # Ausnahme ignorieren
    pass

try:
    f = open('/etc/shadow')
except Exception as e:
    # Ausnahme protokollieren, aber nicht erneut auswerfen
    print('Problem:', e)

print()

try:
    # die im Kommandozeilenargument 1 benannte Datei öffnen
    f = open(sys.argv[1])

except Exception as e:
    # Ausnahme protokollieren und dann mit raise erneut auswerfen
    print('denkste:', e)
    raise

else:
    # alles hat geklappt, dann den Namen der geöffneten Datei ausgeben
    print('alles prima:', f.name)

finally:
    # wird immer ausgeführt; eine unbehandelte Ausnahme wird danach erneut
    # ausgeworfen
    print('finally')

print()

# dividiere die Zahl 5 durch ein positives geradzahliges d
def div5(d):
    assert d >= 0, 'negativer Divisor ist unzulässig'
        # oder kurz:
        # assert d >= 0
    
    if d % 2:
        raise ValueError('ungerader Divisor')

    return 5 / d

# div5 für mehrere Divisoren aufrufen
for div in 1, 3, 0, -4, 0, 5:
    print(f'{div}: ', end = '')
    try:
        print(div5(div))

    except AssertionError as e:
        print(f'{e}, nochmal mit negiertem Vorzeichen:', div5(-div))

    except ZeroDivisionError as e:
        print(e) # division by zero

    except ValueError as e:
        print(f'{e}, nochmal mit {div+1}:', div5(div + 1))

16. Strings

# Strings

# Begrenzer ' und " bzw. ''' und """ für mehrzeilige Strings sind völlig
# gleichwertig
#
# Strings sind unveränderlich, String-Operationen erzeugen daher neue
# String-Objekte

# String-Verkettung mit + und *

print('a' + (' ' * 20) + 'b')
# die Klammern können entfallen (* bindet stärker als +)
print('a' + ' ' * 20 + 'b')

s = 'abc'
s += 'xyza' # s = s + 'xyza'
print(s)

# bei Literalen ist die Verkettung auch ohne + möglich
print('auto' 'bahn')

# bei mehreren Verkettungen und langen Strings empfehlen sich Listen und join()
s = []
s.append('abc')
s.append('xyza')
s = ''.join(s)
print(s)
print()

# String-Methoden
print(s.replace('a', 'X')) # XbcxyzX
print(s.zfill(20))         # 0000000000000abcxyza
print(s.ljust(20) + '|')   # abcxyza             |
print(s.rjust(20))         #              abcxyza
print(s.upper())           # ABCXYZA
print(s.upper().lower())   # abcxyza

print(s.startswith('a'))        # True
print(s.startswith(('a', 'c'))) # True
print(s.endswith('b'))          # False
print(s.endswith(('x', 'n')))   # False
print(s.find('ba'))             # -1
print()

# zeichenweise ausgeben: Iteration mittels for
for c in s: print(c)

# Slicing
print(s[0], s[-1], s[-2]) # a a z
print(s[2:5])             # cxy
print(s[:5])              # abcxy
print(s[3:])              # xyza
print(s[20:21])           # '' (s[20] erzeugt dagegen IndexError: string index out of range)

# Escape-Sequenzen: \n \r \t \\ \' \" ...
print('guten\ntag')
s = '  hallo allerseits\r\n'
print(repr(s))              # '  hallo allerseits\r\n'
print(repr(s.strip()))      # 'hallo allerseits'
print(repr(s.lstrip()))     # 'hallo allerseits\r\n'
print(repr(s.rstrip('\n'))) # '  hallo allerseits\r'

# reguläre Ausdrücke - Modul re
import re

# wir nutzen hier einen Raw-String (Backslash steht für sich selbst und leitet
# keine Escape-Sequenz ein);
# Muster \ba passt auf ein a am Wortanfang und a\b auf ein a am Wortende
mo = re.search(r'\ba|a\b', s)
if mo:
    # Muster gefunden: Position ausgeben
    print(mo.span()) # (8, 9)

# mit finditer() kann man die Treffer durchlaufen
print(list(re.finditer(r'\ba|a\b', 'b1b a2a c3c')))
# [<_sre.SRE_Match object; span=(4, 5), match='a'>, <_sre.SRE_Match object; span=(6, 7), match='a'>]

# den String mit den Index-Werten seiner Zeichen ausgeben
for i, c in enumerate(s): print(i, c)

# Musterersetzung
print(re.sub('a.*r', '', s))    # ab dem ersten a alles löschen
print(re.sub(r'\ba.*r', '', s)) # vom ersten a am Wortanfang bis zum letzten r alles löschen

# Strings in Listen aufspalten und Listen zu Strings zusammensetzen
print(s.split())
print(s.split('a'))

# join() verlangt Strings als Argumente
l = 'guten\ntag'.splitlines()
print(l)
print('|'.join(l))

# mit einem Generatorausdruck wandeln wir die Elemente von range(10) in Strings um
print('/'.join(str(n) for n in range(10)))

# Ausgabe in eine Datei
open('mylist', 'w').write('\n'.join(l))
    # anonmyes Dateiobjekt wird vom Garbage Collector geschlossen und beseitigt

with open('myfile.txt', 'w') as f:
    for line in l:
        print(l, file = f)

# ohne with:
# f = open('myfile.txt', 'w')
# ...
# f.close()

# Datei zeilenweise einlesen und Zeilen an die Liste anhängen
for line in open('mylist'):
    l.append(line)

print(l)

# Datei nochmal lesen und deren Zeilen an die Liste anhängen
l.extend(open('mylist'))

# mehrzeiliger String
mzs = '''
Python
ist
toll
'''
print(mzs)
print(mzs.strip())

17. Kodierung und Ausgabe von Strings

# String-Kodierung und -Ausgabe

import sys
import io

text = '123äöüß €'

# einen String UTF8-kodiert auf die Standardausgabe schreiben; Dateiumlenkung
# ist kein Problem
print(text)

# Ausgabe in eine UTF8-kodierte Datei (Standard)
with open('utf8_file', 'w') as f:
    f.write(text + '\n')
    print(text, file = f)

# Ausgabe in eine Latin1-kodierte Datei
#
# € existiert bei Latin 1 nicht:
#   UnicodeEncodeError: 'latin-1' codec can't encode character '\u20ac' in position 8: ordinal not in range(256)
#
# mit 'replace' werden ungültige Zeichen durch ? ersetzt
with open('latin1_file', 'w', encoding = 'latin1', errors = 'replace') as f:
    f.write(text + '\n')

# Binär-Ausgabe (Bytes statt String)
with open('latin1_file', 'ab') as f:
    # String explizit mit encode() kodieren
    f.write((text + '\n').encode('latin1', 'replace'))
    # alternativ über bytes()
    f.write(bytes('ä€Ü\n', encoding = 'latin1', errors = 'replace'))
    # oder als Bytes-Literal
    f.write(b'***\n')

# alternativ: Nutzung von io.FileIO
with io.FileIO('latin1_file', 'a') as f:
    f.write(b'===')

# Bytes-Objekte sind unverändliche Sequenzen von Integers des Intervalls [0, 255]
for i in b'AB C': print(i)

# Kodierung für Standard-Ausgabe festlegen
#
# extern über die Umgebungsvariable PYTHONIOENCODING möglich:
#
#   PYTHONIOENCODING=utf-8 python3 somescript.py
#
# im Python-Code:
sys.stdout = open(sys.stdout.fileno(), mode = 'w', encoding = 'latin1', errors = 'replace', buffering = 1)
print(text)
sys.stdout.write(text + '\n')
sys.stdout.buffer.write((text + '\n').encode('latin1', 'replace'))
sys.stdout.buffer.flush()

# Byte-String von der Standard-Eingabe lesen und auf die Standard-Ausgabe
# ausgeben
data = sys.stdin.buffer.read()
print(type(data), file = sys.stderr) # <class 'bytes'>
sys.stdout.buffer.write(data[:100])  # maximal die ersten 100 Bytes ausgeben

18. Rechnen mit Python

#!/bin/env python3

# Rechnen mit Python

# die Namen des aktuellen Sichtbarkeitsbereichs ausgeben
print(dir(), end='\n\n')

# die Module math, decimal und cmath für mathematische Funktionen, Dezimal- und
# komplexe Zahlen importieren
import math
import decimal
import cmath

# aus dem Modul fractions importieren wir die Klasse Fraction
from fractions import Fraction

print(dir(), end='\n\n')

# die Namen des Moduls math ausgeben
print(dir(math), end='\n\n')

print(math.__doc__, end='\n\n')
# This module is always available.  It provides access to the
# mathematical functions defined by the C standard.

print(math.pi, math.e) # 3.141592653589793 2.718281828459045
math.pi = 3.2 # circa :-)
print(math.pi)

print(math.log(math.e))

# 2 Float-Zahlen aus Strings erstellen
netto = float('17.2345')
mwst = float('10')
brutto = netto * (1 + mwst / 100)

print('\nBrutto:', brutto)                                 # 18.957950000000004
print('Brutto mit 2 Stellen: %.2f' % brutto)               # 18.96
print('Brutto gerundet auf 3 Stellen: ', round(brutto, 3)) # 18.958
print()

# 0o11   --> Oktalzahl
# 0x0a   --> Hexadezimalzahl (auch 0xa möglich); wahlweise Groß-/Kleinschreibung
#            bei Präfix und Hexadezimalziffern
# 1.8E5  --> float-Zahl in Exponentenschreibweise; e kann auch klein geschrieben werden
print((1 + 0o11) * 0x0a, 1.35, 1.8E5, (3 + 1j) * 3, 3 ** 4)

# Ganzzahlen vom Typ "int" unterstützen Langzahlarithmetik
print(2 ** 1024)

print(0b11, 0B11)  # Binärzahl 11 ==> 3
print(0o72, 0O72)  # Oktalzahl 72 ==> 58
print(bin(10))     # ==> '0b1010'

print('\nkomplexe Zahlen')
c = 3 + 4j # oder: 3 + 4J
# kartesische Koordinaten (Real- und Imaginärteil)
print(c.real, c.imag)

# Polarkoordinaten
print(cmath.polar(c))
# (5.0, 0.9272952180016122)

# alternative Bestimmung der Polarkoordinaten
print(abs(c), cmath.phase(c))

# Umwandlung der Polarform in die algebraische (kartesische) Form
print(cmath.rect(*cmath.polar(c)))
print(cmath.rect(cmath.polar(c)[0], cmath.polar(c)[1]))

print('\nDivision')
# // ==> ganzzahlige Division
# %  ==> Rest der Division (Modulo-Operator)
print(1/3, 1 // 3, 7 % 3)
print()

a = 12.1
b = 2
c = a / b
print(c)       # 6.05
print(str(c))  # 6.05
print(repr(c)) # 6.0499999999999998 bis Python 2.6 und 6.05 ab Python 2.7
               # repr() wird im interaktiven Modus benutzt

print('\nDecimal')
a = decimal.Decimal(str(a))
b = decimal.Decimal(str(b))
c = a / b
print(repr(c))         # Decimal("6.05")
print(c, end = '\n\n') # 6.05

# Rundungsungenauigkeit bei float
z = '0.33333333333333333333'
print(decimal.Decimal(z) * 3) # Decimal('0.99999999999999999999')
print(float(z) * 3)           # 1.0

print(0.1 + 0.2 == 0.3) # False
print(decimal.Decimal('0.1') + decimal.Decimal('0.2') == decimal.Decimal('0.3')) # True

# die Operationen mit Decimals kann man ziemlich fein über den Context steuern;
# hier ein Beispiel:

# den Default-Kontext auslesen
default_context = decimal.getcontext()
# einen neuen Kontext einstellen, der eine abweichende Präzision aufweist
decimal.setcontext(decimal.Context(prec = 5))

# in einer Schleife jeweils ein Zahlenpaar addieren
for z1, z2 in ('1.11111111', '2.22222222'), ('3.33333333', '4.44444444'):
    a = decimal.Decimal(z1)
    b = decimal.Decimal(z2)
    print(a)
    print(b)
    print(a + b)
    print('gerundet' if decimal.getcontext().flags[decimal.Rounded] else 'exakt')
    # den Default-Kontext wieder aktivieren
    decimal.setcontext(default_context)

# Ausgabe:
#   1.11111111
#   2.22222222
#   3.3333
#   gerundet
#   3.33333333
#   4.44444444
#   7.77777777
#   exakt

# mit with kann man die Präzision bequem auf einen Anweisungsblock begrenzen
print('\nwith')
with decimal.localcontext(decimal.Context(prec = 5)):
    print(decimal.Decimal('1') / decimal.Decimal('3')) # 0.33333

print(decimal.Decimal('1') / decimal.Decimal('3'))   # 0.3333333333333333333333333333

# rationale Zahlen (Brüche)
print('\nFraction')
f1 = Fraction(6, 8)                       # Fraction(3, 4)
f2 = 2 * f1                               # Fraction(3, 2)
print('%s + %s = %s' % (f1, f2, f1 + f2)) # Fraction(9, 4)
# 3/4 + 3/2 = 9/4

19. None und Boolean

# None und True/False

n = None

if not None:
    print('bool(None) ist immer False')

print('' == None) # False
print(0 == None)  # False
print()

# Identität
print('' is None)        # False
print(0 is None)         # False
print(id(0), id(None))   # id() liefert die Speicheradresse eines Objekts
print(id(0) == id(None)) # False
print()

# True / False
print(0 == False)        # True
print(0 != False)        # False
print(0 is True)         # False
print(0 is not True)     # True
print(not 0 is True)     # True
print()

print(not 1 is True)     # True ("is" bindet stärker als "not")
print(not (1 is True))   # True
print((not 1) is True)   # False
print((not 0) is True)   # True
print()

# mit True/False kann man rechnen
print(int(True), int(False)) # 1 0
print(0 + True)              # 1
print(0 + False)             # 0
print(True + False)          # 1
print()

# bei Vergleichen mit Zahlen werden True/False nach 1/0 konvertiert
print(1 == True)  # True, da 1 == int(True)
print(0 == False) # True, da 0 == int(False)
print(1 is True)  # False, da 1 und True verschiedene Objekte sind
print(0 is False) # False, da 0 und False verschiedene Objekte sind
print()

# Boolesche Ausdrücke
#
# x or y  ==> wenn x falsch, dann y, sonst x
# x and y ==> wenn x falsch, dann x, sonst y
# not x   ==> wenn x falsch, dann True, sonst False
#
# Operatorpräzedenz:
#   1. not
#   2. and
#   3. or
#
# short-circuit evaluation: sobald das Ergebnis feststeht, endet die Auswertung
# des Booleschen Ausdrucks

print(4 and 0 or 5)     # 5 ("and" bindet stärker als "or")
print(4 or 0 and 5)     # 4
print((0 or 1) and 5)   # 5
print(0 or None and 5)  # None
print(0 or True and 5)  # 5
print(0 or False and 5) # False
print(0 and 1 / 0)      # 0 (kein ZeroDivisionError, da 1 / 0 nicht ausgewertet wird)
print()

if 0 and True:
    # mit "0 and" kann man Code-Teile deaktivieren
    print('nie erreicht')

if 1 or False:
    # und mit "1 or" stets aktivieren
    print('stets erreicht')

print(not 5)            # False
print(not not 5)        # True
print(4 or 9)           # 4
print(not not (4 or 9)) # True

20. Tupel

# Tupel - unveränderliche Sequenzen
#
# Zweck:
# - effizienter als Listen
# - nutzbar als Keys von Dictionaries
# - manche Funktionen fordern Tupel, z.B. die String-Methoden startswith/endswith
# - auch beim Formatierungs-Operator % werden Tupel gefordert

# leeres Tupel
tupel0 = ()
print(tupel0)

# einelementiges Tupel (Singleton) mit Komma am Ende
tupel1 = 1,
print(tupel1)

# mehrelementiges Tupel
tupel = 1, 2, 3

for i in 1, 2, 3: print(i)
print()

# explizite Tupel-Klammerung bei nicht leeren Tupeln generell möglich, aber bei
# Tupel-Zuweisungen und Schleifen nicht nötig
tupel1 = (1,)
tupel = (1, 2, 3)
for i in (1, 2, 3): print(i)
print()

# Indexierung
print(tupel[1], tupel[-1])

# Slicing
print(tupel[0:2])

# Sequence Unpacking
a, b, c = tupel
print(a, b, c)

# Vertauschen zweier Variableninhalte
a = 3
b = 4
a, b = b, a # Tupel (b, a) wird nach a und b entpackt
print(a, b)

# Tupel kennen + und *
print(tupel + tupel, tupel * 2)
print(tupel + tuple('abcd') + tuple([4, 5, 6]))
print()

# Formatierung mit %: Tupel erforderlich
print('%d %d %d' % tupel)
if 0:
    print('%d %d %d' % list(tupel)) # TypeError: %d format: a number is required, not list
    print('%s %s %s' % list(tupel)) # TypeError: not enough arguments for format string

# mit format() statt %
print('{:d} {:d} {:d}'.format(*tupel))

# Python 3 unterstützt Extended Iterable Unpacking
a, *b, c = 2 * tupel
print(a, b, c)        # 1 [2, 3, 1, 2] 3
a, b, *c = tupel + (4, 5)
print(a, b, c)        # 1 2 [3, 4, 5]

21. Listen

# Listen - veränderliche Sequenzen

import sys
import re
from pprint import pprint

# Listen aus Modul sys - Modulsuchpfad und Argumenteliste
print(sys.path)
print(sys.argv)
print()
print(len(sys.argv))
pprint(sys.argv)
pprint(sys.path)
print()

# Listen ganzer Zahlen
print(list(range(10)))
print(list(range(-5, 5)))
print(list(range(-5, 5, 2)))
print(list(range(20, 2, -3)))
print()

# leere Liste
l = []         # oder: l = list()

# Listen anfügen
l.extend(list(range(20)))
l += dir()[-2:]         # l = l + dir()[-2:]
print(l)
print()

# Elemente anfügen, einfügen, löschen
l.append(10)         ; print(l)
l.insert(0, 'start') ; print(l)
l.pop()              ; print(l)
l.pop(0)             ; print(l)
del l[2]             ; print(l)
del l[:3]            ; print(l)
del l[8:]            ; print(l)
del l[2:5]           ; print(l)
print()

# ermittle, ob und ggf. wo bestimmte Elemente in der Liste stehen
for x in 'start', 2, 4:
    if x in l:
        print(x, l.index(x))

# entferne Elemente aus der Liste
for x in 'start', 2, 4:
    if x in l:
        l.remove(x)

print(l)
print()

# Konvertierung Tupel <==> Liste
print(tuple(l))
print(list(tuple(l)))
print()

# Unpacking einer Liste
date = '2012-12-11'
j, m, t = date.split('-')

# formatierte Ausgabe
print('%s . %s . %s' % (t, m, j))                          # 11 . 12 . 2012
print('%s . %s . %s' % tuple(reversed(date.split('-'))))   # 11 . 12 . 2012
print('{} . {} . {}'.format(*(reversed(date.split('-'))))) # 11 . 12 . 2012

# Unpacking einer durch Slicing gebildeten Liste
a, b = list(range(10))[-2:]
print(a, b)
print()

# Sortieren

l = ['ccx', 'aaay', 'dz', 'bba', '']
k = l[:] # k bekommt eine (flache) Kopie von l;
         # copy.deepcopy() bietet eine tiefe Kopie
k.sort() # k wird sortiert und somit verändert, l ist davon nicht betroffen
print(l)
print(k)
print()

# sortierte Kopien von l erstellen

print(sorted(l))                         # steigend sortieren
print(list(reversed(sorted(l))))         # fallend sortieren
print(sorted(l, reverse = 1))            # sorted kann selber fallend sortieren
print(sorted(l, key = len))              # nach der Länge der Listenelemente sortieren
print(sorted(l, key = lambda x: x[-1:])) # nach dem letzten Element (Zeichen) jedes Listenelementes sortieren;
                                         # Slicing funktioniert auch bei leerem String, x[-1] dagegen nicht
print()

# statt der anonymen lambda-Funktion kann man auch eine normale Funktion
# nutzen, die das letzte Element (Zeichen) eines Strings (bzw. einer Sequenz)
# ermittelt
def lastchar(s):
    return s[-1:]

# damit kann man dann sortieren
print(sorted(l, key = lastchar))
print()

# Listen-Manipulation:

# Slicing
l = list(range(5, 100, 5)) # [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
print(len(l))              # 19
print(l[:5])               # [5, 10, 15, 20, 25]
print(l[:10:2])            # [5, 15, 25, 35, 45]
print(l[15:])              # [80, 85, 90, 95]
print(l[10::-1])           # [55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5]
print(l[::-1])             # [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5]
print(l[10::5])            # [55, 80]
print(l[8:14])             # [45, 50, 55, 60, 65, 70]
print(l[8:14:2])           # [45, 55, 65]
print(l[::3])              # [5, 20, 35, 50, 65, 80, 95]
print(l[::-3])             # [95, 80, 65, 50, 35, 20, 5]
print()

print('Elemente einzeln einfügen')
l[1:3] = list(range(5))    # [5, 0, 1, 2, 3, 4, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
print(l)

print('Liste einfügen')
l[2:4] = [list(range(6))]  # [5, 0, [0, 1, 2, 3, 4, 5], 3, 4, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
print(l)
print(l[2][-1]) # das letzte Element des 3. Elements von l ausgeben
print()

# Listen in Listen
l = [1, 3, ['a', None, False], (9, 8)]
for i in l:
    print(i, type(i))

l[2][2] = True
print(l)

l[2] = [3, 4] # Liste wird als ein Element eingefügt
print(l)
print()

l = [list(range(2)), list(range(3)), list(range(4))]
# verschachtelte Listen durch verschachtelte for-Schleifen ausgeben
for i in range(len(l)):
    for j in range(len(l[i])):
        print('l[%d,%d] = %s' % (i, j, l[i][j]))

print()

# alternativ:
for i, sublist in enumerate(l):
    for j, item in enumerate(sublist):
        print('l[%d,%d] = %s' % (i, j, item))

print()

# List Comprehension: Listenobjekte funktional/deklarativ beschreiben;
# erinnert an die mathematische Notation von Mengen

print([2 * x for x in range(-5, 5) if x > 0])

# "x and y" entspricht "x != 0 and y != 0"
print([(x, y) for x in range(4) for y in range(3) if x and y])

# nur die mit A beginnenden der auf l endenden Strings der Ausgangsliste in die
# neue Liste aufnehmen
print([s for s in ['Anton', 'Berta', 'Caesar', 'Alf', 'Emil'] if re.search('^A|l$', s)])
# ['Anton', 'Alf', 'Emil']

# durch extended slicing jedes 3. Element entnehmen
print([(x, y) for x in range(4) for y in range(3) if x and y][::3])

def nkz(line):
    'Nutzerkennzeichen aus passwd-Zeile herausspalten'
    return line.split(':')[0]

print([nkz(line) for line in open('/etc/passwd')][:5])
print([line.split(':')[0] for line in open('/etc/passwd')][:5])

print()

# Generatorausdrücke
# () statt []
#
# dienen der Beschreibung von Generatoren, die im Rahmen einer Iteration die
# benötigten Elemente der Reihe nach erzeugen; eine List Comprehension erzeugt
# dagegen stets eine komplette Liste aller zu generierenden Elemente

go = (2 * x for x in range(-5, 5) if x > 0) # <generator object <genexpr> at 0x7f69a6229048>
for x in go: print(x)
print()

# analog dem obigen Beispiel zu /etc/passwd; Generator ist bei langen Dateien effizienter
go = (line.split(':')[0] for line in open('/etc/passwd'))
for uid in range(5): print(next(go))
print()

# oder:
for i, uid in zip(range(5), (line.split(':')[0] for line in open('/etc/passwd'))):
    print(uid)
print()

# die Klammerung des Generatorausdrucks kann entfallen, wenn er das einzige
# Funktionsargument ist
print(sum(2 * x for x in range(-5, 5) if x > 0))
print(list(2 * x for x in [y for y in range(-5, 5) if y > 0]))

# Generatorausdruck innerhalb eines Generatorausdrucks
print(sum(2 * x for x in (y for y in range(-5, 5) if y > 0)))

# List Comprehension innerhalb eines Generatorausdrucks
print(sum(2 * x for x in [y for y in range(-5, 5) if y > 0]))

# maximale und minimale UID in der /etc/passwd ermitteln
print(max(int(line.split(':')[2]) for line in open('/etc/passwd')))
print(min(int(line.split(':')[2]) for line in open('/etc/passwd')))

# Skalarprodukt zweier Vektoren
xvec = [10, 20, 30]
yvec = [7, 5, 3]
print(sum(x * y for x, y in zip(xvec, yvec)))
    # list(zip(xvec, yvec)) liefert [(10, 7), (20, 5), (30, 3)]
print()

# eigene Generatorfunktion mit yield

def num_gen(n):
    for x in range(n):
        yield x * x

# for iteriert über dem Generator, der durch den Aufruf num_gen(10) erzeugt wurde
for x in num_gen(10): print(x, end = ' ')
print()

# analog mit Generatorausdruck
for x in (x * x for x in range(10)): print(x, end = ' ')
print()

# eine Liste mit dem Generator initialisieren
print(list(num_gen(5)))

# zu Fuß
g = num_gen(10) # führt num_gen() bis zum ersten yield aus und liefert einen
                # Generator, der das Iterator-Interface bietet

# Endlosschleife
while 1:        # while True: ...
    try:
        print(next(g), end = ' ') # das nächste Element vom Generator anfordern
    except StopIteration as e:
        # die StopIteration behandeln, die ausgeworfen wird, wenn der Generator
        # erschöpft ist
        print(type(e)) # <class 'StopIteration'>
        # Abbruch der Schleife
        break

22. Dictionaries

# Dictionaries

# - Dictionaries speichern Schlüssel-Wert-Paare
# - beliebige Objekte als Werte nutzbar
# - Schlüssel müssen aber unveränderliche Objekte sein

d = {
  'auto' : 'car',
  'baum' : 'tree',
  'tür'  : 'door',
}

d['raum'] = 'ROom' # legt hier ein neues Schlüssel-Wert-Paar an
print(d)
d['raum'] = 'room' # ändert den Wert für den existierenden Schlüssel "raum"
print(list(d.keys()))     # Liste der Schlüssel
print(list(d.values()))   # Liste der Werte
print(list(d.items()))    # Liste der Schlüssel-Wert-Paare (Tupel)

d[(1,2)] = 'Tupel als Key'
# Liste als Key geht nicht!
# d[[1,2]] = 'Liste als Key'
# TypeError: unhashable type: 'list'

# erstes und drittes Feld (Kennzeichen und UID) der ersten 5 Zeilen der
# /etc/passwd in einem Dictionary speichern
d = dict(line.split(':')[:3:2] for line in list(open('/etc/passwd'))[:5])
#d = dict(line.split(':')[:3:2] for line in open('/etc/passwd').readlines()[:5])

# Iteration über den Schlüsseln des Dictionarys
#for nkz in d: print('%-20s%06d' % (nkz, int(d[nkz])))
for nkz in d: print(f'{nkz:<20}{int(d[nkz]):06}')
print()

# mittels Iterator die Schlüssel-Wert-Paare (Tupel) des Dictionarys durchlaufen
# und diese dabei gleich entpacken
#for nkz, uid in d.items(): print('%-20s%06d' % (nkz, int(uid)))
for nkz, uid in d.items(): print(f'{nkz:<20}{int(uid):06}')
print()

if 'root' in d: print(d['root']) # Test auf Schlüssel 'root'
del d['root']                    # Schlüssel 'root' löschen
if 'root' in d: print(d['root']) # ohne Test käme hier ein KeyError: 'root'
print(d.get('root'))             # bei get() kommt dagegen kein NameError, es liefert None oder
print(d.get('root', -1))         # einen Vorzugswert (hier -1) bei fehlendem Schlüssel 'root'
print()

# leeres Dictionary
d = {}

users = 'root', 'anton', 'root', 'berta'

for nkz in users:
    # im Dictionary zählen, wie oft ein Wort vorkommt; die Wörter dienen als
    # Schlüssel, der Wert ist die Anzahl
    d[nkz] = d.get(nkz, 0) + 1
print(d)

from collections import defaultdict, Counter

# bequemer mit defaultdict
dd = defaultdict(int)
for nkz in users:
    dd[nkz] += 1
print(dd)

# noch bequemer mit Counter
cnt = Counter(users)
print(cnt)

# die Wörter nochmal zählen (Update des Counters)
cnt.update(users)

# Dictionary Comprehension
d = {x: x*x for x in range(5)}
print(d)

d1 = {x : x**2 for x in range(1, 10)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

d2 = {x : x**3 for x in range(4)}
# {0: 0, 1: 1, 2: 8, 3: 27}

# Dictionary Updates
d.update(Marta = 23, Emma = 77)
d = dict(d, Otto = 88, Adam = 43)
d.update([('Jutta', 44), ('Jens', 17)])
d.update((('Max', 99), ('Moritz', 100)))
d.update((x, x + 1) for x in range(2))     # mit Generatorausdruck
print(d)

# Dictionaries mischen (ab Python 3.5)
d3 = {**d1, **d2}
# {1: 1, 2: 8, 3: 27, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 0: 0}

d4 = {1: 1111, 2: 2222, **d1} # d1 == d4
d4 = {**d1, 1: 1111, 2: 2222}
# {1: 1111, 2: 2222, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

# die Menge der Key beider Dictionaries
s = {*d1, *d2}
# {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

23. Invertierung eines Dictionarys

# ein Dictionary invertieren: Werte werden zu Schlüsseln und Schlüssel zu
# Werten

# Original-Dictionary
d = {
   1: 'eins',
   2: 'zwei',
   3: 'drei',
  '3': 'drei',
}

# Dictionary ausgeben
print(d)

# Variante 1: das invertierte Dictionary über eine Dictionary Comprehension
# generieren; man verliert aber Werte, wenn wie oben ein Wert des
# Original-Dictionarys mehreren Schlüsseln zugeordnet ist:
print({d[key] : key for key in d})
#   {'eins': 1, 'zwei': 2, 'drei': '3'}  ==> die Integer-Zahl 3 geht hier verloren
#
# korrekt wäre:
#
#   {'eins': [1], 'zwei': [2], 'drei': [3, '3']}

# Variante 2 erzeugt dieses korrekte Dictionary

# ein leeres invertiertes Dictionary anlegen
inv_d = {}

# wir durchlaufen alle Schlüssel-Wert-Paare des Original-Dictionarys; da
# mehreren Schlüsseln des Original-Dictionarys derselbe Wert zugeordnet sein
# kann, verwalten wir im invertierten Dictionary pro Key eine Liste von Werten,
# um keine Daten zu verlieren
for key, val in d.items():
    if val not in inv_d:
        # der aktuelle Wert des Original-Dictionarys existiert noch nicht als
        # Key des invertierten Dictionarys, daher legen wir ihn an und weisen
        # ihm die leere Liste als Wert zu
        inv_d[val] = []
    # den aktuellen Schlüssel des Original-Dictionarys als Wert an die
    # Werte-Liste anhängen
    inv_d[val].append(key)

print(inv_d)

# alternativ zur eigenen Implementierung oben kann man ein defaultdict aus dem
# Modul collections nutzen
from collections import defaultdict

# als Default-Element nutzen wir die Klasse list, also eine leere Liste; diese
# wir einem noch nicht existierenden Schlüssel automatisch beim ersten Zugriff
# als Wert zugewiesen
inv_dd = defaultdict(list)

# der obige Test auf die Existenz des Schlüssels und die explizite Zuweisung
# der leeren Liste entfällt hier
for key, val in d.items():
    inv_dd[val].append(key)

# Ausgabe des defaultdict-Objekts
print(inv_dd)

# Ausgabe des daraus erstellten normalen Dictionarys
print(dict(inv_dd))

24. Mengen

# Mengen

# eine Menge über ein Set-Literal erzeugen
s1 = {1, 2, 3, 4, 5, 6}

# ein Element hinzufügen
s1.add(7)

# das vorhandene Element 6 mit remove() löschen
s1.remove(6)
print(s1)

# ein nochmaliges Löschen schlägt fehl, da es das Element 6 nicht gibt
try:
    s1.remove(6)
except Exception as e:
    if 0:
        # mit pass können wir die Ausnahme ignorieren
        pass
    else:    
        # wir melden die Ausnahme
        print(type(e), e)

print(s1)

# bei discard() statt remove() existiert dieses Problem nicht, dort wird keine
# Ausnahme generiert
s1.discard(6)
print(s1)

# Set Comprehension
s = {(x, y) for x in range(2) for y in range(3)}
print(s)

# 2 Sets generieren
s1 = set(range(10))
s2 = frozenset(range(5, 15)) # eine unveränderliche Menge

# 3 Varianten, die dieselbe Menge erzeugen
s3 = set([x ** 3 for x in range(10)]) # via List Comprehension
s3 = set(x ** 3 for x in range(10))   # über einen Generatorausdruck
s3 = {x ** 3 for x in range(10)}      # mit Set Comprehension

print(s1 & s2) # Schnittmenge
print(s2 & s3)
print(s1 | s2) # Vereinigungsmenge
print(s1 - s2) # Differenz
print(s1 ^ s2) # symmetrische Differenz (Vereinigungsmenge - Schnittmenge)

# Ermittlung der Python Keywords
#   import keyword
#   keyword.kwlist
keywords_python2 = [
  'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif',
  'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import',
  'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try',
  'while', 'with', 'yield'
]

keywords_python3 = [
  'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break',
  'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally',
  'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal',
  'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield'
]

kw2 = frozenset(keywords_python2)
kw3 = frozenset(keywords_python3)

print('\nKeywords nur in Python 2')
print(kw2 - kw3)

print('\nKeywords nur in Python 3')
print(kw3 - kw2)

print('\nsymmetrische Differenz der Keywords von Python 2 und 3')
print(kw3 ^ kw2)

print('\nsortierte, dublettenfreie Liste aller Keywords von Python 2 und 3')
print(sorted(frozenset(keywords_python2 + keywords_python3)))

# eine fallend nach der Elementanzahl sortierte Liste der Tupel
# (Anzahl, Element) generieren
l = list(range(10)) + list(range(20)) + list(range(15))
print(
  sorted(
    # mit set(l) oder frozenset(l) erhalten wir die dublettenfreie Menge der
    # Listenelemente
    [(l.count(x), x) for x in frozenset(l)],
    reverse = True
  )
)

# mit Generatorausdruck statt List comprehension; da der Generatorausdruck hier
# nicht das einzige Funktionsargument ist, muss er in runde Klammern
# eingeschlossen werden
print(sorted(((l.count(x), x) for x in frozenset(l)), reverse = True))

25. Funktionen

# nutzerdefinierte Funktionen in Python

# globale Variable, die ein unveränderbares (immutable) Objekt referenziert
g = 100

# Addition von x zu g
def add_global(x):
    # durch die Anweisung "global" beziehen sich Zuweisungen an g auf die globale
    # Variable g
    global g
    g += x      # Kurzform von g = g + x
    # ohne return wird immer None als Funktionswert geliefert

print('g', g)

# den Funktionswert (hier None) fangen wir nicht auf --> Nutzung einer Funktion
# als Prozedur
add_global(10)
print('g nach add_global(10)', g)
print('Wert von add_global(20)', add_global(20))
print('g', g)

def set_lokal(x):
    # ohne die Anweisung "global" erfolgt die Zuweisung an g immer lokal
    g = 2 * x
    print('g in set_lokal', g)

set_lokal(20)
print('g nach set_lokal(20)', g)

def print_g():
    # das globale g ist auch ohne die Anweisung "global" lesbar
    print('print_g', g)

print_g()
print()

# Funktion mit 2 Parametern; Parameter b bekommt einen Vorzugswert
def produkt(a, b = 2):
    return a * b

print('3 * 4 =', produkt(3, 4))
print('3 * 2 =', produkt(3))

# Nutzung von Schlüsselwort-Parametern statt Positionsparametern; beim
# Funktionsaufruf sind evtl. vorhandene Positionsparameter stets vor den
# Schlüsselwort-Parametern anzugeben
print('AB * 5 =', produkt(b = 5, a = 'AB'))  # AB * 5 = ABABABABAB
print('AB * 5 =', produkt('AB', b = 5))      # AB * 5 = ABABABABAB
#print('AB * 5 =', produkt(b = 5, 'AB'))     # SyntaxError: positional argument follows keyword argument

# eine globale Variable, die auf ein veränderbares (mutable) Objekt verweist
g_mutable = []

# Funktion, die den Wert einer globalen Variablen verändert
def add_to_list(x):
    # auch ohne die Anweisung "global" kann man das durch g_mutable referenzierte
    # Objekt modifizieren
    g_mutable.append(x)

print('g_mutable', g_mutable)
add_to_list(11)
add_to_list('hallo')
print('g_mutable nach add_to_list', g_mutable)
print()

# Rückgabe eines Tupels als Funktionswert; so kann eine Funktion n Werte
# zurückliefern (auch Mengen, Dictionaries, Listen oder nutzerdefinierte
# Objekte sind dafür nutzbar)
def ret_n_werte(x):
    return x, x.upper(), x.lower(), x + '|' + x

res = ret_n_werte('Auto')
print(res)

# Funktion mit beliebiger Anzahl von Positions- und Schlüsselwort-Parametern
def flexibel(*par, **sw_par):
    print('Positionsparameter', par)         # ein Tupel
    print('Schlüsselwort-Parameter', sw_par) # ein Dictionary

print('\nflexibel')
# auch beim Funktionsaufruf sind * und ** mit Tupeln bzw. Dictionaries nutzbar
flexibel(1, 'zwei', (1, 2), a = 23, b = [1, 2, 3], c = set([11, 22]), *(1, 2, 3), **{'d': 200, 'e': 400})

# Ausgabe:
# Positionsparameter (1, 'zwei', (1, 2), 1, 2, 3)
# Schlüsselwort-Parameter {'a': 23, 'b': [1, 2, 3], 'c': {11, 22}, 'd': 200, 'e': 400}

# Debug-Ausgabe mit print() generieren
import sys
def debug(*args, **kwargs):
    print(*args, file = sys.stderr, **kwargs)

debug('Fehler', end = ' ')
debug('Warnung')
#debug('Hinweis', file = sys.stdout) # TypeError: print() got multiple values for keyword argument 'file'

# Python 3 kennt im Gegensatz zu Python 2 "keyword-only arguments" (reine
# Schlüsselwort-Parameter): das sind alle formalen Parameter nach * bzw. *par
# und vor dem optionalen **sw_par
def kwonly(a, *, b, **sw_par):
  # mit *par statt * könnte man zusätzliche Positionsparameter auffangen
  print(a, b, sw_par)

# kwonly(1, 2)               # TypeError: kwonly() takes 1 positional argument but 2 were given
kwonly(1, b = 2)             # 1 2 {}
kwonly(a = 1, b = 2)         # 1 2 {}
kwonly(b = 2, a = 1, c = 9)  # 1 2 {'c': 9}

# die formale Parameterliste kann also der Reihe nach enthalten:
#
#  1. Positionsparameter mit oder ohne Vorzugswert
#
#  2. genau einen *-Parameter (* oder *par)
#
#  3. reine Schlüsselwort-Parameter mit oder ohne Vorzugswert, die nur nach
#     einem *-Parameter folgen dürfen und dem Parameter * statt *par folgen
#     müssen
#
#       def kwonly(a, *, **sw_par):
#                       ^
#       SyntaxError: named arguments must follow bare *
#
#  4. genau einen **-Parameter

# eine rekursive Funktion
def fakultaet(n):
    n = int(abs(n))
    if n in (0, 1): # oder: if n < 2:
        # hier terminiert die Rekursion
        return 1
    return n * fakultaet(n - 1)

for i in -1.3, 0, 0.5, 1, 3, 7, 10:
    print(i, fakultaet(i))
print()

# eine äußere Funktion kann auch einen Verweis auf die innere Funktion
# zurückliefern
def make_adder(n = 5):
    def adder(x):
        # n ist der Parameter der äußeren Funktion
        return x + n
    # die äußere Funktion make_adder() liefert eine Closure zurück: eine Funktion
    # mit Bindung an einen Kontext für ihre nicht-lokalen Variablen
    return adder

add_3 = make_adder(3)
add_5 = make_adder()
sub_1 = make_adder(-1)
print('add3(97) =', add_3(97))    # add3(97) = 100
print('add5(95) =', add_5(95))    # add5(95) = 100
print('sub_1(100) =', sub_1(100)) # sub_1(100) = 99

from functools import wraps
    # s. https://docs.python.org/3.6/library/functools.html#functools.wraps

# Dekoratorfunktion; die Dekoration wird über eine Closure realisiert
def add_log(f):
    # ohne den Dekorator @wraps() hätte app_elem.__name__ den Wert "wrapper" und
    # app_elem.__doc__ den Wert "der Wrapper, der um f gelegt wird"; der
    # Original-Docstring ginge also verloren
    @wraps(f)
    def wrapper(li, el):
        'der Wrapper, der um f gelegt wird'
        print('füge %s an %s an' % (el, li))
        f(li, el)
    return wrapper

# Nutzung des Dekorators;
# @add_log entspricht app_elem = add_log(app_elem)
@add_log
def app_elem(liste, elem):
    'ein Element an eine Liste anhängen'
    liste.append(elem)

print()
l = []
app_elem(l, 55)
app_elem(l, 66)
print(l)

print(app_elem.__name__, '==>', app_elem.__doc__)

26. Start-Beispiel OOP

#!/bin/env python3
#
# einfaches OOP-Beispiel

# eine eigene Zähler-Klasse definieren
class counter(object):
    'Klasse zur Realisierung eines Zählers'

    # generell sollten nur new-style classes genutzt werden, also jene, die von
    # object abgeleitet sind; bei Python 2 muss man die Basisklasse object
    # explizit angeben, bei Python 3 kann diese Angabe entfallen, da es dort
    # keine old-style classes mehr gibt:
    # 
    #   class counter(): ...

    # Klassenvariable mit Vorzugs-Startwert 0 anlegen; sie kann von außen
    # überschrieben werden
    init_val = 0

    # der erste Parameter jeder Instanz-Methode erhält die Referenz auf das
    # jeweilige Objekt und sollte immer 'self' genannt werden

    def __init__(self, *args):
        # Konstruktor/Initialisierer (constructor/initializer):
        # Initialisierung eines neuen Zähler-Objekts; der Startwert wird im
        # Instanz-Dictionary gespeichert; Vorzugs-Startwert ist das Klassenattribut
        # init_val
        self.val = args[0] if args else self.init_val

    def count(self, n = 1):
        # Zähler um n inkrementieren
        self.val += n

    def get(self):
        # Zählerstand auslesen
        return self.val

# Ableitung einer Klasse von einem eingebauten Datentyp
class plist(list):
    'plist ist eine Liste, die alle append-Operationen protokolliert'

    # Methode append() von Klasse list wird überschrieben
    def append(self, x):
        print('appending', x)
        # Methode append() der Basis- bzw. Superklasse wird gerufen
        super().append(x)
        # bei Python 3 kann super() parameterlos gerufen werden, bei Python
        # benötigte super() 2 Argumente, z.B.:
        #   super(plist, self).append(x)
        # ohne das empfohlene super() ginge es auch so:
        #   list.append(self, x)

# 3 Counter-Objekte erzeugen
c1 = counter(5)       # Startwert 5
c2 = counter()        # Startwert 0
counter.init_val = 9  # Vorzugs-Startwert in der Klasse auf 9 setzen
c3 = counter()        # Startwert 9

for c in c1, c2, c3:  # Zählerstände über get() auslesen
    print(c.get())

c1.count()            # Zähler um 1 erhöhen
print(c1.get())

c1.count(8)           # Zähler um 8 erhöhen
print(c1.get())

# die Instanz-Attribute sind öffentlich, man kann sie direkt auslesen oder
# manipulieren
print(c1.val, c2.val, c3.val)
c1.val += 10
c2.val -= 1
c3.val *= 5
print(c1.val, c2.val, c3.val)

c1.val, c2.val, c3.val = 5, 10, 15
c1.val *= 8
print(c1.val, c2.val, c3.val)

# Anzeige des Instanz-Dictionarys objekt.__dict__
for o in c1, c2, c3:
    print(vars(o), o.__dict__)

# mit dir() sieht man die Namen der Instanz-Attribute, wobei im Gegensatz zu
# vars() auch die Basisklassen betrachtet werden
print(dir(c1))

# Hinweis: die innerhalb einer Klassendefinition verwendeten Namen von
# Attributen und Methoden, die mit 2 Unterstrichen beginnen und nicht mit
# mindestens 2 Unterstrichen enden, unterliegen einem sog. name mangling; dabei
# wird der Name mit einem Präfix versehen, das aus einem Unterstrich gefolgt
# vom Klassennamen besteht; durch dieses "Verstecken" lassen sich
# Namenskollisionen vermeiden; die Namen bleiben aber weiter öffentlich 

class C(object):
    __x = 10  # name mangling: aus _x  wird _C__x

print(C()._C__x)

print()

# eine Instanz von plist anlegen
pl = plist()
print(pl)

# Element protokolliert anfügen
pl.append(88)
print(pl)

print()

# Test auf Klassenzugehörigkeit
print(isinstance(pl, counter)) # False
print(isinstance(pl, plist))   # True
print(isinstance(pl, list))    # True

# Test auf Subklasse
print(issubclass(plist, list))     # True
print(issubclass(plist, object))   # True
print(issubclass(plist, counter))  # False
print(issubclass(counter, object)) # True

print()

# Ermittlung der Klasse und des Tupels aller Basisklassen sowie der MRO (method
# resolution order)
for o in c1, pl:
    print(o)
    print('__class__ ==>', o.__class__)
    print('__bases   ==>', type(o).__bases__)
    print('__mro__   ==>', type(o).__mro__)
    print()

27. Minimalistische Klasse als Daten-Container

# Nutzung einer minimalistischen Klasse ohne Attribute/Methoden als
# Daten-Container

# StringIO gestattet es, Strings wie Dateien zu lesen und zu schreiben
from io import StringIO

# operator-Funktion für den Attributzugriff importieren
from operator import attrgetter

# unsere minimalistische Klasse
class data(object):
    pass

# Text-Datendatei mit zeilenweisen Personenangaben; alternativ kann man die
# Daten in einer externen Textdatei ablegen
persons = '''
Anton Meier 1972
Barbara Lämmel 1961
Annerose Lehmann 1954
'''

# eine Liste als Container von data-Objekten
person_list = []

# die Text-Daten einlesen und die Personenliste aufbauen
for line in StringIO(persons):
    line = line.strip()
    if line:
        # data-Objekt erzeugen
        pers = data()
        # 3 Attribute von außen setzen
        pers.firstname, pers.name, pers.year = line.split()[:3]
        # das Jahr in eine ganze Zahl konvertieren
        pers.year = int(pers.year)
        # an die Personenliste anhängen
        person_list.append(pers)

# Funktion zur Ausgabe einer Personenliste
def print_list(pers_list):
    for pers in pers_list:
        print(f'{pers.year:4} {pers.name}, {pers.firstname}')

# die Personenliste aufsteigend sortiert nach dem Jahr ausgeben
print_list(sorted(person_list, key = lambda x: x.year))

print()

# analog mit attrgetter() statt lambda
print_list(sorted(person_list, key = attrgetter('year')))

print()

# nur jene Personen ausgeben, deren Jahr kleiner als 1970 ist
print_list(sorted((pers for pers in person_list if pers.year < 1970), key = attrgetter('year')))

28. spezielle Sichtbarkeitsregeln im Klassen-Körper

# spezielle Sichtbarkeitsregeln im Klassen-Körper

a = 'global a'

class X():
    # außerhalb von Methoden erfolgt der Zugriff auf Attribute der Klasse über
    # einfache Namen; in Methoden sind dagegen für Klassen- und Instanz-Attribute
    # immer voll qualifizierte Namen zu verwenden

    a = 'a in class'
    b = a.replace('a ', 'b ')
    # X.a statt a ist nicht zulässig, da X nicht sichtbar ist
    #
    # NameError: name 'X' is not defined

    def hello(self):
        print('hello ', self)

    def print_vars(self, my = a): # hier ist X.a als a sichtbar
        print(my)                 # a in class
        try:
            hello(1)              # hello() ist nicht sichtbar
        except Exception as e:
            print(repr(e))        # NameError("name 'hello' is not defined",)
        X.hello(2)                # X.hello() ist sichtbar
        print(self.a)             # a in class
        print(X.a)                # a in class
        print(a)                  # global a
        print(self.b)             # b in class
        print(b)                  # NameError: name 'b' is not defined

    # im Klassenkörper ist hello() sichtbar
    hello(3)

x = X()
x.print_vars()

29. Klasse mit Property und speziellen Methoden

# Klasse zur Verwaltung von Personen
class Person():
    # Konstruktor/Initialisierer
    def __init__(self, alter, groesse, name = None):
        self.alter = alter
        self.groesse = groesse
        self.name = name

    # String-Repräsentation einer Person erstellen
    def __repr__(self):
        return repr((self.alter, self.groesse, self.name))

    # einfache String-Repräsentation einer Person erstellen
    def __str__(self):
        return '%s/%s/%s' % (self.alter, self.groesse, self.name)

    # Person altern lassen, also Alter um n Jahre erhöhen
    def altern(self, n = 1):
        self.alter += n

    # mittels eines Dekorators eine Property mytuple erzeugen;
    # Properties implementieren das Deskriptor-Protokoll
    @property
    def mytuple(self):
        # das ist der Getter; den Namen lassen wir hier im Rückgabewert aus
        return self.alter, self.groesse
    # alternativ:
    #   def mytuple(self): return self.alter, self.groesse
    #   mytuple = property(mytuple)

    # einen Setter für die Property definieren
    @mytuple.setter
    def mytuple(self, tup):
        if tup[0] > 10 and tup[1] > 150:
            self.alter, self.groesse = tup[:2]
            if len(tup) > 2:
                # wenn t einen Namen enthält, dann diesen auch setzen
                self.name = tup[2]

    # einen Deleter für die Property definieren
    @mytuple.deleter
    def mytuple(self):
        self.alter = self.groesse = 0
        self.name = ''

# Personenliste erstellen
personen = [Person(39, 172, 'ABC'), Person(88, 165), Person(15, 181), Person(88, 175)]

# Ausgabe der Personenliste
print(personen)
print()

# Attribut mytuple der letzten Person der Liste löschen
del personen[-1].mytuple

# Iteration über der Personenliste und Ausgabe der einzelnen Personen
for pers in personen:
    print(pers, '==>', repr(pers))
print()

# alle Personen altern lassen
for pers in personen:
    pers.altern(3)

# nochmal ausgeben
print('nach dem Altern')
print(personen)
print()

# nochmal altern lassen, diesmal funktional
list(map(lambda x: x.altern(3), personen))
print('nach dem 2. Altern')
print(personen)
print()

# nochmal funktional altern lassen, diesmal mit Vorzugswert n
list(map(Person.altern, personen))
print('nach dem 3. Altern')
print(personen)
print()

# Ausgabe der sortierten Personenliste
print(sorted(personen, key = lambda pers: (pers.alter, pers.groesse)))
print()

# dito mit benannter Funktion statt einer anonymen lambda-Funktion
def pers_key(pers):
    return pers.alter, pers.groesse

print(sorted(personen, key = pers_key))

# Attribute sind public, man kann von außen zugreifen
print(personen[0].alter)
print(personen[0].groesse)
print(personen[0].name)

p = personen[0]
print(p.alter + p.groesse)

# Nutzung der Property mit Getter
print(p.mytuple)
p.alter += 100
print(p.mytuple)

# Nutzung des Setters der Property
p.mytuple = 1, 2 # wird vom Setter stillschweigend ignoriert
print(repr(p.mytuple))

p.mytuple = 11, 155
print(repr(p.mytuple))

# nochmal, aber mit Name
p.mytuple = 11, 155, 'Pumuckl'
print(repr(p.mytuple))

for p in personen:
    print(p)

print('maximales Element einer Personen-Liste bestimmen')
print(max((person.alter, person.groesse) for person in personen))
print()

# Größe von außen ändern
personen[1].groesse += 5
for p in personen:
    print(p)
print()

# neues Attribut setzen
personen[1].name2 = 'XYZ'
for p in personen:
    print(p) # __str__() wird für die String-Darstellung gerufen
print()

# hier sieht man das neue Attribut
for p in personen:
    print(vars(p))
print()

print('Maximum der Property mytuple')
print(max(person.mytuple for person in personen))
# oder:
print(max(personen, key = lambda p: (p.alter, p.groesse)).mytuple)

# das Tupel der letzten Person der Liste ändern
personen[-1].mytuple = 110, 190

# Maximum erneut ausgeben
print('Maximum der Property mytuple nach Zuweisung')
print(max(person.mytuple for person in personen))
print()

# nochmal alle Attribute mit vars()
for p in personen:
    print(vars(p))

30. Gebundene und ungebundene Methoden

#!/bin/env python
# -*- coding: utf8 -*-

"""
gebundene und ungebundene Methoden
"""

from __future__ import print_function
import types

liste = [1, 2, 3]

# die Methode "append" gebunden und ungebunden bereitstellen
gebunden = liste.append
ungebunden = liste.__class__.append # entspricht: list.append

print(gebunden)       # <built-in method append of list object at 0x...>
print(ungebunden)     # <method 'append' of 'list' objects>

# Aufruf der gebundenen Methode
gebunden(4)           # eine 4 an die Liste anhängen
print(liste)          # [1, 2, 3, 4]

# Aufruf der ungebundenen Methode
ungebunden(liste, 5)  # eine 5 an die Liste anhängen
print(liste)          # [1, 2, 3, 4, 5]

# Benutzerdefierte Funktionen besitzen eine Methode __get__() und sind daher
# non-overriding descriptors. Die Deskriptor-Methode __get__() wird beim
# Zugriff auf ein entsprechendes Attribut eines Objekts (z.B. liste.append)
# gerufen und liefert eine gebundene Methode. Man kann __get__() auch direkt
# rufen:

def f(): pass
print(f.__get__('')) # Python 2: <bound method ?.f of ''>
                     # Python 3: <bound method f of ''>

# eigene Listen-Klasse vom eingebauten Typ "list" ableiten; hier sieht man die
# "bound method" bzw. "unbound method" explizit
class mylist(list):
    # Methode append() wird überschrieben
    def append(self, x):
        print('appending', x)
        # Methode append() der Basis- bzw. Superklasse wird gerufen
        super(mylist, self).append(x)
        # bei Python 3 kann man super() ohne Argumente nutzen:
        #   super().append(x)
        #
        # ohne das empfohlene super() ginge es auch so:
        #  list.append(self, x)

# eine Instanz von "mylist" kreieren
ml = mylist((1, 2, 3))
print(ml)

# die Methode 'append' wieder gebunden und ungebunden bereitstellen
gebunden = ml.append
ungebunden = mylist.append

print(gebunden)         # <bound method mylist.append of [1, 2, 3]>
print(ungebunden)       # Python 2: <unbound method mylist.append>
                        # Python 3: <function mylist.append at 0x...>
print(type(gebunden))   # Python 2: <type 'instancemethod'>
                        # Python 3: <class 'method'>
print(type(ungebunden)) # Python 2: <type 'instancemethod'>
                        # Python 3: <class 'function'>

print(isinstance(gebunden, types.MethodType))   # True
print(isinstance(ungebunden, types.MethodType)) # Python 2: True
                                                # Python 3: False

# Aufruf der gebundenen Methode
gebunden(4)           # appending 4
print(ml)             # [1, 2, 3, 4]
ml.append(5)          # appending 5
print(ml)             # [1, 2, 3, 4, 5]

# Aufruf der ungebundenen Methode
ungebunden(ml, 6)     # appending 6
print(ml)             # [1, 2, 3, 4, 5, 6]
mylist.append(ml, 7)  # appending 7
print(ml)             # [1, 2, 3, 4, 5, 6, 7]

# eine Standard-Liste weiß von unserem mylist.append() nichts
l = list((1, 2, 3))
l.append(99)
print(l)              # [1, 2, 3, 99]

# Bei nutzerdefinierten Klassen werden die gebundenen und bei Python 2 auch die
# ungebunden Methoden durch ein Objekt vom Typ types.MethodType repräsentiert.
# Das ist ein dünner Wrapper um ein normales Funktionsobjekt, der diese
# Attribute definiert:
#
#   m.__doc__   Dokumentations-String
#   m.__name__  Name der Methode
#   m.__class__ Klasse, in der die Methode definiert wurde
#   m.__func__  Funktionsobjekt, das die Methode implementiert
#   m.__self__  Instanz, an die die Methode gebunden ist (None im Falle von
#               ungebundenen Methoden)
#  
# Ab Python 3 fehlt bei ungebundenen Methoden der Wrapper vom Typ
# types.MethodType. Es handelt sich hier um das reine Funktionsobjekt, das die
# Methode implementiert. Außerdem erfolgt für den Parameter self keine
# Typprüfung mehr, während Python 2 einen TypeError wirft, wenn man der
# ungebundenen Methode ein Objekt übergibt, das keine Instanz der Klasse ist,
# für die man die ungebundene Methode ruft.

class liste():
    def append(self):
      'special append'
      pass

l = liste()
gebunden = l.append
ungebunden = liste.append

for obj in gebunden, ungebunden:
    # __name__ taucht bei dir() hier nicht auf, wir ergänzen es daher
    attribute = dir(obj) + ['__name__']
    for name in '__doc__', '__name__', '__class__', '__func__', '__self__':
        if name in attribute:
            attr = getattr(obj, name)
            print(name, attr, type(attr))
    print()

# Python 2:
# 
#   __doc__ special append <type 'str'>
#   __name__ append <type 'str'>
#   __class__ <type 'instancemethod'> <type 'type'>
#   __func__ <function append at 0x...> <type 'function'>
#   __self__ <__main__.liste instance at 0x...> <type 'instance'>
#   
#   __doc__ special append <type 'str'>
#   __name__ append <type 'str'>
#   __class__ <type 'instancemethod'> <type 'type'>
#   __func__ <function append at 0x...> <type 'function'>
#   __self__ None <type 'NoneType'>
#  
# Python 3:
#
#   __doc__ special append <class 'str'>
#   __name__ append <class 'str'>
#   __class__ <class 'method'> <class 'type'>
#   __func__ <function liste.append at 0x...> <class 'function'>
#   __self__ <__main__.liste object at 0x...> <class '__main__.liste'>
#  
#   __doc__ special append <class 'str'>
#   __name__ append <class 'str'>
#   __class__ <class 'function'> <class 'type'>

31. Nutzung eines Overriding Descriptors

# -*- coding: utf8 -*-

# Nutzung eines Overriding Descriptors

class OverridingDescriptor(object):
    'einfacher data/overriding descriptor'

    # ein non-overriding descriptor hat nur eine Methode __get__(), aber kein
    # __set__() und wird beim attribut lookup etwas anders behandelt

    # wir verwalten hier die Attributwerte in den Descriptor-Instanzen
    def __init__(self, val = 0):
        self.val = val

    # __get__ wird beim attribute lookup gerufen
    def __get__(self, instance, cls):
        print('__get__ in descriptor: %s, %s, %s' % (self, instance, cls))
        # wir geben hier das Doppelte des gespeicherten Wertes zurück
        return 2 * self.val

    # __set__ wird beim attribute binding gerufen
    def __set__(self, instance, value):
        print('__set__ in descriptor: %s, %s, %s' % (self, instance, value))
        self.val = value

    # __del__ wird bei del gerufen
    def __delete__(self, instance):
        print('__delete__ in descriptor: %s, %s' % (self, instance))

# Definition einer Klasse, die den obigen Descriptor nutzt
class MyClass(object):
    x = OverridingDescriptor(4)
    y = 5

m = MyClass()
print(m.x)  # 8
print(m.y)  # 5

m.x = 40
print(m.x)  # 80

del m.x     # es wird nichts gelöscht
print(m.x)  # 80

# Analyse der Objekte

# das Instanz-Dictionary von m ist leer
print()
print('vars(m)', vars(m))

print()
# bei dir() sehen wir u.a. die beiden Klassenattribute x und y
print('dir(m)', dir(m))

print()
# dir() wird für das von der Descriptor-Methode __get__ gelieferte Objekt
# gerufen (im Beispiel also für 80)
print('dir(m.x)', dir(m.x))

print()
# analog zu dir(m.x), wobei der Zugriff hier über die Klasse statt über die
# Instanz erfolgt
print('dir(MyClass.x)', dir(MyClass.x))

print()
# beim Zugriff über das Klassen-Dictionary greifen wir direkt auf die
# Descriptor-Instanz zu und sehen, wie sie den Attributwert speichert; __get__
# wird hier also nicht gerufen
print("vars(MyClass.__dict__['x'])", vars(MyClass.__dict__['x']))

4. Sammlung von Code-Beispielen


Holger Trapp

letzte Modifikation: 09.11.2023