#!/usr/bin/env python
# coding: utf-8

# # Schleifen (loops)

# Schleifen sind Strukturelemente, die ein wiederholtes Durchlaufen von Programmblöcken ermöglichen. Man unterscheidet while-Schleifen und for-Schleifen.
# 
# In der letzten Woche hatten wir gesehen, dass auch durch Verwendung eines rekursiven Funktionsaufrufs ein wiederholtes Abarbeiten von Anweisungen möglich ist. Für viele Anwendungen sind Schleifen aber der offensichtlichere Ansatz um Anweisungen wiederholt auszuführen. 

# ## for-Schleifen
# Der Aufbau einer for Schleife ist wie folgt, wobei das __else__ optional ist
# ```python
#     for element in iterierbarers_objekt:
#         block von anweisungen
#     [else]
#         block von anweisungen
# ```

# In[1]:


for i in [1, 2, 4]:
    print(i)


# In[2]:


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


# Das geht einfacher mit dem Range Objekt. Damit erhält man alle ganzen Zahlen in einem Bereich.
# ```python 
# range([anfang],ende)
# ```
# Wird der Anfangsindex nicht angegeben, beginnt der Bereich bei 0. Der Endeindex ist wieder exklusiv.

# In[3]:


list(range(1, 4))


# In[4]:


for i in range(1, 4):
    print(i)


# Auch Wörterbücher, Mengen und Zeichenketten sind iterierbar

# In[5]:


wb = {1: 'a', 2: 'b', (1, 2): 'a+b', 0: 'Null'}
for k in wb:
    print(k, wb[k])


# In[6]:


menge = {1, 3, 4, 1, 2}
for element in menge:
    print(element)


# In[7]:


for zeichen in '+-+-':
    print(zeichen)


# __print__ beendet jede Ausgabe mit einer neuer Zeile. Mit dem _end_ Paramater kann man das Ende der Ausgabe festlegen.

# In[8]:


for zeichen in '+-+-':
    print(zeichen, end=',')


# # Einschub
# 
# Mit 
# ```python 
# n = 10
# f'abc {n}'
# ```
# erzeugt man den String 'abc 10', wobei der Ausdruck in { } ausgewertet wird

# In[9]:


jahr = 2024
f'Willkommen in {jahr}'


# ### __break__ und __continue__ 
# Mit __break__ kann man eine Schleife vorzeitig verlassen und mit __continue__ springt man zur nächsten Iterierten und überspringt den Rest.

# In[10]:


for n in range(1, 10):
    if n % 4 == 0:
        print(f'{n} = 4*{n//4}')
        break
    if n % 2 == 0:
        print(f'{n} = 2*{n//2}')


# In[11]:


for n in range(1, 10):
    if n % 4 == 0:
        print(f'{n} teilbar durch 4: {n} = 4*{n//4}')
        continue
    if n % 2 == 0:
        print(f'{n} teilbar durch 2: {n} = 2*{n//2}')


# Das optionale __else__ in einer for Schleife wird selten verwendet. Es ist vermutlich nur nützlich, wenn man mit einem __break__ eine Schleife abbricht.
# 
# Beispiel aus https://docs.python.org/3/tutorial/controlflow.html Abschnitt 4.5

# In[12]:


for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:  # x teilt n
            print(f'{n} ist {x} * {int(n/x)}')
            break
    else:  # die Schleife  x in 2..n-1 wurde nicht abgebrochen oder die Schleife ist leer
        print(f'{n} ist prim')


# Es kommt etwas anderes raus, wenn wir das __else__ "verschieben"

# In[13]:


for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:  # x teilt n
            print(f'{n} ist {x} * {int(n/x)}')
            break
        else:  # hier gehört das else zum if, alsoe falls x kein Teiler von n ist
            print(f'{n} ist prim')


# oder weglassen

# In[14]:


for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:  # x teilt n
            print(f'{n} ist {x} * {int(n/x)}')
            break
        print(f'{n} ist prim')


# ### Nützliche built-in Funktionen
# Built-in Funktionen, die das Programmieren mit __for__-Schleifen vereinfachen, sind
# __zip()__, __enumerate()__, __reversed()__, __sorted()__.
# 
# 
# __zip()__ verbindet mehrere Sequenzen und erlaubt ein gemeinsames Iterieren. 

# In[15]:


str1 = 'AEIOU'
lst1 = ['Affe', 'Esel', 'Igel', 'Ohrenkneifer', 'Uhu', 'Zebra']

for i, j in zip(str1, lst1):
    print(f'{i} wie {j}')


# __reversed()__ dreht die Reihenfolge um. 

# In[16]:


for i in reversed(lst1):
    print(i)


# __enumerate()__ zählt die durchiterierten Elemente ab

# In[17]:


for i, j in enumerate(lst1):
    print(i, j)


# In[18]:


for i, j in enumerate(lst1,
                      start=1):  # Man kann auch bei start zu zählen beginnen
    print(i, j)


# In[19]:


list(enumerate(zip(str1, lst1)))


# ## while Schleife

# Der Aufbau einer while Schleife ist wie folgt, wobei das __else__ optional ist. Der Block anweisung1 wird ausgeführt solange bedingung __True__ ist.
# ```python
#     while bedingung:
#         anweisung1
#     [else]
#         anweisung2
# ```
# Wieder kann man mit __break__  die Schleife abbrechen oder mit __continue__ zum Anfang der Schleife springen. 

# In[20]:


a = 1
print(a)
while a <= 10:
    a = a + (a + 1)
    print(a)


# In[21]:


a = 1
print(a)
while True:
    if a > 10:
        break
    a = a + (a + 1)
    print(a)


# In[22]:


def arek(a):
    print(a)
    if a > 10:
        return a
    return arek(a + (a + 1))


a = arek(1)


# In[23]:


from numpy.random import randint


# In[24]:


zaehler = 0
summe = 0
while summe < 100:
    summe += 1 + randint(6)  # += inplace plus (summe = summe + 1 + randint(6))
    zaehler += 1
print(f'Nach {zaehler} mal würfeln ist die Augensumme {summe}')


# ## Listen-Abstraktion
# ### für Listen (list comprehension)
# Statt

# In[25]:


a = [[1]]
for _ in range(5):
    a.append([1])
a


# verwendet man auch die kompaktere Form

# In[26]:


aa = [[1] for _ in range(5)]
aa


# ### für Wörterbücher (dictionary comprehension)

# In[27]:


{i: i**2 for i in range(5)}


# ### für Mengen (set comprehension)

# In[28]:


{i**2 for i in range(5)}


# man kann dabei auch Bedingungen abfragen

# In[29]:


{i**2
 for i in range(5) if (i**2) % 2 > 0}  # alle ungeraden Quadratzahlen bis 16


# ### Generator

# In[30]:


g = (i**2 for i in range(5))


# In[31]:


list(g)


# Generatoren brauchen weniger Speicher, da Sie faul (lazy) sind und das jeweils nächste Element erst berechnen, wenn es benötigt wird.

# Primzahlen bis N

# In[47]:


N = 20
[n for n in range(2, N) if all((n % m > 0 for m in range(2, n)))]


# ## Ein Beispiel: Minimum

# In[33]:


def mymin(a, b):
    """ Berechnet das Minimum der Eingabeparameter"""
    if a < b:
        return a
    else:
        return b


# In[34]:


mymin(3.2, 2.1)


# ### Packing und Unpacking
# Mit __\*__ kann man beliebig viele Eingabeparameter in eine Liste zusammenfassen

# In[35]:


*b, = 1, 2, 3
b


# In[36]:


lst = list(range(5))
a, b, *c = lst
c, a, b


# In[37]:


def mymin(*eingabewerte):
    #print(eingabewerte, type(eingabewerte))
    minimum = abs(eingabewerte[0])
    for a in eingabewerte:

        assert isinstance(
            a, (float, int, complex)), 'Eingabewerte müssen Zahlen sein'

        if abs(a) < minimum:
            minimum = abs(a)

    return minimum


# In[38]:


mymin(2, 2.1, 3, 1 + 2j)


# In[39]:


lst


# Der __\*__ dient auch dazu eine Liste zu entpacken

# In[40]:


print(*lst)


# In[41]:


liste = [2, 3, 4, 1]


# In[42]:


mymin(*liste)


# In[43]:


mymin(liste)


# In[44]:


min(liste)  # eingebaute Minimumfunktion in Python


# In[45]:


get_ipython().run_line_magic('pinfo', 'min')


# In[ ]:




