# 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__)
