# Lekcja 9-10: Funkcje

## Spis treści

1. Co to jest funkcja?

- 1.1. Funkcja jako "maszynka"

- 1.2. Do czego służą funkcje?
  - Modularność
  - Prostota
  - Nazewnictwo funkcji

- 1.3. Wywoływanie funkcji
  - Przykład: Funkcje do konwersji typów
  - Przykład: Funkcje wbudowane
  - Przykład: Metody to też funkcje!
  - Przykład: Podstawowe funkcje matematyczne

2. Definiowanie funkcji

- 2.1. `def`
  - Ogólna składnia
  - Uwaga techniczna: `def` jest stwierdzeniem wykonywalnym

- 2.2. Wariacje na temat `return`
  - Funkcje zwracające wiele rezultatów jednocześnie
  - Wielokrotny `return`
  - Funkcje nic nie zwracające
  - Instrukcja `pass`
  - Funkcje czyste i modyfikatory

- 2.3. Wariacje na temat argumentów
  - Przekazywanie argumentów do funkcji
  - Argumenty pozycyjne (wymagane)
  - Argumenty nazwane
  - X
  - X
  - X
  - X

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

## 1. Co to jest funkcja?

### 1.1. Funkcja jako "maszynka"

Aby wprowadzić pojęcie **funkcji** ("function"), powinniśmy na chwilę wrócić do szkolnej matematyki.

Ze szkoły na pewno pamiętasz wyrażenia typu $f(x) = 2x + 5$. Mamy tu funkcję o **nazwie** ("name") $f$, która przyjmuje **argument** ("argument") o nazwie $x$, będący liczbą, a **zwraca** ("returns") wynik, w tym przypadku rezultat obliczenia $2x + 5$. Podstawiając za $x$ konkretną liczbę, np. $x = 7$, otrzymujemy konkretny liczbowy rezultat, tutaj: $f(7) = 2 \cdot 7 + 5 = 19$.

Można zatem funkcję wyobrazić sobie jako swoistą "maszynkę", do której z jednej strony wrzucamy "składniki"/argumenty, na których następnie "maszynka" dokonuje serię zdefiniowanych operacji, aby z drugiej strony wyrzucić "gotowy produkt"/rezultat.

Funkcje w Pythonie generalnie działają dokładnie w ten sam sposób: należy dostarczyć im argumenty, na których przeprowadzane są wcześniej zdefiniowane operacje, a zwracany jest wynik tych operacji. Mogą to zrobić jednak w sposób dużo bardziej wszechstronny niż funkcje matematyczne!

### 1.2. Do czego służą funkcje?

#### Modularność

Z punktu widzenia programistycznego, taka "maszynka" to _wyodrębniony blok kodu, który wykonuje konkretnie zdefiniowaną czynność._ I jak to "maszynka", ma ona dwa podstawowe zastosowania:

1. Duże maszyny składają się z mniejszych elementów, a te z jeszcze mniejszych elementów itd. Dzięki funkcjom możemy rozbić duży, złożony problem na wiele małych, dobrze określonych, łatwiejszych do zrozumienia i zakodowania części ("procedural decomposition"). Np. programując robota przygotowującego pizzę, zamiast jednej długiej procedury `make_pizza`, lepiej rozbić ją na logiczne kawałki, `mix_dough`, `roll_out` , `add_toppings`, `bake` itp. Łatwiej takim kodem zarządzać, łatwiej szukać w nim błędów, łatwiej go skalować. Funkcje implementują zatem zasadę ["DRY = Don't Repeat Yourself"](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

2. Maszyny wyręczają nas w powtarzalnych czynnościach. Funkcja opisuje konkretną czynność, a zatem jest naturalnym obiektem wielokrotnego użytku - definiujemy ją raz, a możemy wywoływać wielokrotnie ("code reusability"). Również jeśli tę czynność chcemy później zmodyfikować, robimy to tylko raz - w ciele funkcji - a nie w każdym miejscu jej użycia. Co więcej, użyteczna funkcja może przydać się w kolejnych naszych projektach, nie tylko w danym programie.

A zatem funkcje są bardzo uniwersalnym i podstawowym narzędziem strukturyzującym kod.

#### Prostota

Od razu praktyczna uwaga: Każda nasza funkcyjna "maszynka" powinna:

- być w miarę prosta i czytelna; ważnym kryterium jest po prostu liczba linijek kodu składająca się na funkcję - powinna ona być w miarę niska;

- nie robić zbyt wielu rzeczy naraz - a najlepiej tylko jedną rzecz!

Tylko wówczas będzie dobrze spełniać swoją rolę strukturyzowania kodu i unikania powtórzeń.

Jeśli w trakcie pisania programu stworzyliśmy zbyt długą i przez to niezbyt zrozumiałą funkcję, spróbujmy przepisać ją na serię mniejszych kawałków - robiących koniec końców to samo, ale w bardziej czytelnej formie. Proces takiego przepisania nazywa się **"refactoring"**.

#### Nazewnictwo funkcji

Dobrą praktyką programowania jest nadawanie funkcjom nazw złożonych z pełnych wyrazów angielskich, które dobrze opisują, co dana funkcja robi. Jest to inaczej niż w matematyce, gdzie funkcje zwykle nazywa się pojedynczymy literami, $f$, $g$ itp.

- Dozwolone są wielkie i małe litery, cyfry, oraz podkreślnik `_`, z tym że nazwa nie może zaczynać się od cyfry. Nazwą nie może być też słowo zarezerwowane w Pythonie, takie jak `list`, `return` itp. - one już oznaczają coś konkretnego.

- Nie powinno się stosować skrótów, jako że utrudniają one zrozumienie nazwy przez osobę inną niż autor danego kodu.

- Większość edytorów ma funkcję "auto-complete", a więc nawet jeśli nazwa funkcji będzie długa, to wystarczy wpisać ją w całości tylko raz, a później używać automatycznego uzupełniania, tj. zacząć pisać nazwę, a edytor dokończy ją za nas, po naciśnięciu odpowiedniego klawisza, np. `Tab`.

### 1.3. Wywoływanie funkcji

Zobaczmy najpierw kilka przykładów funkcji. Zanim nauczymy się "konstruować"/definiować własne "maszynki"/funkcje, zobaczmy jak je "uruchamiać"/**wywoływać** ("call"). Podobnie jak w matematyce - np. $f(x)$ - robimy to poprzez nazwę funkcji i umieszczenie wartości jej argumentów w nawiasach okrągłych.

#### Przykład: Funkcje do konwersji typów

Jedną z pierwszych poznanych przez nas funkcji była funkcja `type`, która przyjmuje jeden argument - dowolny obiekt - a zwraca jego typ danych, np.:

```python
type( 55 )
```

```python
type( 'I love functions!' )
```

```python
type( { 'name' : 'John' , 'age' : 22 } )
```

Wielokrotnie też spotykaliśmy się z funkcjami służącymi do konwersji typów. W czasie tego kursu poznaliśmy wiele różnych rodzajów - **typów** ("types") - obiektów, np. liczby całkowite (typ `int`), liczby zmiennoprzecinkowe (typ `float`), stringi (typ `str`), wartości logiczne (typ `bool`), listy (typ `list`), tuple (typ `tuple`), zbiory (typ `set`), słowniki (typ `dict`) itd. Funkcje do konwersji typów mają takie same nazwy, jak typ, _na jaki_ chcemy konwertować. Np.:

```python
str( 55 ) # konwersja int na str
```

```python
int( '55' ) # konwersja str na int
```

```python
list( 'abc' )
```

```python
set( [ 1 , 2 , 3 , 1 , 2 , 3 ] )
```

```python
bool( '' )
```

#### Przykład: Funkcje wbudowane

W Pythonie mamy oczywiście wiele funkcji, które ktoś wcześniej dla nas zdefiniował - są to tzw. **funkcje wbudowane** ("built-in functions"), tu jest ich [spis](https://docs.python.org/3/library/functions.html). Część z nich już często używaliśmy, choćby `len`, `sum`, `max`, `min`; mają one jeden argument, będący kolekcją, a zwracają odpowiednio długość, sumę elementów i maksymalny/minimalny element kolekcji (jeśli pytanie o to ma sens). Np.:

```python
len( { 'name' : 'John' , 'age' : 22 } )
```

```python
sum( [ int( digit ) for digit in str( 12345 ) ] )
```

```python
max( 'honorificabilitudinitatibus' )
```

... itd.

Kilka innych przykładów: Funkcja wbudowana o nazwie `abs` przyjmuje jeden argument liczbowy i zwraca jego wartość bezwzględną:

```python
abs( -7.8 )
```

Funkcja wbudowana `pow` przyjmuje dwa argumenty i zwraca pierwszy z nich podniesiony do potęgi równej drugiemu z nich:

```python
pow( 4 , 2 )
```

Poznaliśmy też funkcje `range` i pamiętamy, iż może one być wywołana z jednym, dwoma albo trzema argumentami, a zwraca odpowiedni ciąg liczb całkowitych, który po konwersji na listę wygląda tak:

```python
list( range( 10 ) )
```

```python
list( range( 5 , 10 ) )
```

```python
list( range( 5 , 10 , 2 ) )
```

Zauważmy tu dwie rzeczy: Po pierwsze, liczba argumentów funkcji `range` waha się od jednego do trzech, ni mniej, ni więcej - zobaczymy później, jak definiować funkcje, które mogą przyjmować _różną liczbę argumentów_. Po drugie, wywołaliśmy tu funkcję `list` z argumentem będącym rezultatem wywołania funkcji `range`.

Poznaliśmy też funkcję `enumerate`, przyjmującą jako argument kolekcję uporządkowaną - np. string, listę, czy tuplę - a zwracającą listę tupli 2-elementowych:

```python
list( enumerate( 'abc' ) )
```

Inna funkcja wbudowana, `zip`, może przyjmować _dowolną_ liczbę argumentów będących kolekcjami uporządkowanymi i zwraca listę tupli zawierających kolejno ich pierwsze elementy, ich drugie elementy itd.

```python
list( zip( 'quiz' , 'hazy' , 'jack' , 'lazy' , 'haze' ) )
```

Również później zobaczymy, jak definiować funkcje, które mogą przyjmować _nieokreśloną z góry liczbę argumentów_.

#### Przykład: Metody to też funkcje!

W poprzednich Lekcjach wielokrotnie używaliśmy funkcji, ale także metod - i nigdy dokładnie nie wytłumaczyliśmy różnic między nimi. Są to pojęcia bardzo podobne - metoda to także funkcja, lecz "powiązana" z określonym typem danych. Mamy więc metody powiązane z typem danych `str`, takie jak `lower`, `join`, `replace` itd. (zob. Lekcja 2). Mamy metody powiązane z typem danych `list`, jak `index`, `count`, `append` (zob. Lekcja 5). Mamy metody powiązane z typem danych `dict`, jak `get` czy `update` (zob. Lekcja 8). Itd.

Składnia wywoływania jest też nieco inna: Funkcje wywołujemy jak wyżej, tj. nazwa funkcji i argumenty w nawiasach okrągłych:
```
function_name(arguments)
```
Metodę natomiast - powiązaną z określonym typem danym - wywołujemy po kropce od obiektu tego typu, po której następuje nazwa metody i nawiasy okrągłe z ewentualnymi innymi argumentami (albo puste w środku, gdy tych argumentów nie ma):
```
obj.method_name(other_arguments)
```
Choć nie robi się tego w praktyce, można metodę wywołać identyczną składnią, jak funkcję - lecz trzeba wówczas:
- podać jej "pełną" nazwę składającą się z nazwy typu, kropki i nazwy metody, np. metoda `count` typu danych `list` nazywa się w pełni `list.count`;
- obiekt `obj`, na którym wywołujemy metodę, podać jako pierwszy argument w nawiasach okrągłych, a po nim dopiero ewentualną resztę argumentów.

Zatem:
```
type_name.method_name(obj, other_arguments)
```

Np. zamiast:

```python
'ABC'.lower()
```

... możemy równie dobrze napisać:

```python
str.lower( 'ABC' )
```

Zamiast:

```python
', '.join( [ 'one' , 'two' , 'three' ] )
```

... możemy napisać:

```python
str.join( ', ' , [ 'one' , 'two' , 'three' ] )
```

Zamiast:

```python
lst = [ 3 , 1 , 3 , 3 , 3 , 2 , 1 , 2 , 3 ]

lst.count( 3 )
```

... możemy napisać:

```python
list.count( lst , 3 )
```

Zamiast:

```python
d = { 'a' : 1 , 'b' : 2 }

d.get( 'c' , -1 )
```

... możemy napisać:

```python
dict.get( d , 'c' , -1 )
```

I tak dalej. To pokazuje, że metody też są funkcjami - ale szczególnymi, bo powiązanymi z konkretnym typem danych, zdefiniowanymi w treści definicji tego typu.

#### Przykład: Podstawowe funkcje matematyczne

Innym przykładem są typowe funkcje matematyczne, od których zaczęliśmy tę lekcję. Nie są one częścią biblioteki standardowej Pythona, ale możemy je **zaimportować** ("import") z **modułu** ("module") o nazwie `math`. Moduł to po prostu plik, który ktoś wcześniej przygotował, a który zawiera m.in. definicje pewnych funkcji. Importowanie oznacza spojrzenie do tego pliku i użycie definicji funkcji tam zawartych. Programowanie w Pythonie najczęściej polega na pracy z konkretnymi modułami zawierającymi przydatne nam rozwiązania; większość rzeczy ktoś już gdzieś napisał! Codzienna praca z Pythona polega więc często na wyszukiwaniu w sieci potrzebnych nam modułów i zapoznawaniu się z ich dokumentacją; proste wyszukanie pozwala nam np. stwierdzić, że opis funkcji zawartych w module `math` znajduje się [tutaj](https://docs.python.org/3/library/math.html).

Zaimportować moduł możemy w całości, co daje nam dostęp do wszystkich funkcji w nim zdefiniowanych. Piszemy wtedy np.:
```
import math
```
co daje nam dostęp do wszystkich funkcji w module o nazwie `math`. Trzeba wówczas jednak pamiętać o następującej zasadzie: wywołanie funkcji odbywa się nie tylko poprzez nazwę funkcji, ale nazwa ta musi poprzedzona być nazwą modułu oraz kropką. Przykładem funkcji w module `math` jest sinus, `sin`, a więc wywołując ją musimy użyć pełnej nazwy `math.sin`.

```python
import math # importujemy moduł math, który jest plikiem zawierającym definicje wielu funkcji matematycznych

math.sin( 0.87 ) # wywołanie funkcji math.sin, a więc funkcji sin z modułu math
```

Import możemy przeprowadzić również w sposób selektywny, a więc zaimportować z danego modułu jedynie potrzebne nam funkcje, a nie cały plik. Piszemy wówczas np.:
```
from math import sin , cos , tan
```
i wtedy do wywołania funkcji używamy jej nazwy _bez_ poprzedzającej nazwy modułu.

```python
from math import sin , cos , tan # z modułu math importujemy tylko kilka funkcji (i nie mamy dostępu do żadnych innych!)

tan( 0.87 ) * cos( 1.15 ) / sin( - 3.45 ) # teraz wywołanie odbywa się po prostu przy użyciu samej nazwy funkcji; nie: math.tan!
```

## 2. Definiowanie funkcji

### 2.1. `def`

#### Ogólna składnia

Oprócz funkcji wbudowanych (np. `abs`, `len`, `sum`, ...), lub też takich, które importujemy z jakiegoś modułu (np. `math.sin`), możemy **definiować własne funkcje** ("user-defined functions", UDFs). Mając w pamięci analogię z "maszynką", która przetwarza argumenty i "wypluwa" wynik, z programistycznego punktu widzenia **definicja funkcji** ("function definition") będzie osobnym blokiem kodu, który:

- nadaje funkcji nazwę;

- zawiera listę argumentów, jakie funkcja przyjmuje;

- definiuje operacje, jakie "maszynka" wykonuje na swoich argumentach;

- definiuje końcowy rezultat, jaki funkcja zwraca.

Ogólna składnia zbiera te wszystkie elementy, rozpoczynając od słowa kluczowego `def`, i zwykle wygląda tak:

```
def function_name(arguments):
    # various operations
    return result

```
Pierwsza linijka - z nazwą `function_name` i argumentami `arguments` - nazywa się **nagłówkiem** ("header"), a cała treść wykonywanych operacji to **ciało** ("body") funkcji, które zawiera w szczególności - po słowie kluczowym `return` - rezultat `result`, który funkcja **zwraca** ("returns"); ciało funkcji określone jest przez wcięcie kodu, identycznie jak przy instrukcjach warunkowych czy pętlach.

Dla przykładu, prosta funkcja matematyczna z początku lekcji może być zdefiniowana jako:

```python
def f( x ): # definicja funkcji o nazwie f z jednym argumentem x

    # ciało funkcji

    y = 2 * x + 5 # argument x jest przekształcany w y

    return y # y jest rezultatem zwracanym przez funkcję
```

... a jej wywołanie jest:

```python
f( 7 ) # wywołanie funkcji o nazwie f z argumentem o wartości 7
```

Ta funkcja jest tak prosta, że całe wyrażenie obliczające rezultat możemy zawrzeć w linijce `return` bez ryzyka uczynienia kodu nieczytelnym.

```python
def f( x ):
    return 2 * x + 5
```

Zobaczmy inny przykład takiej "krótkiej" funkcji - a więc tylko `return` i jedno wyrażenie: Napiszmy funkcję, która ma dwa argumenty: listę `lst` oraz pewną dodatnią liczbę całkowitą `limit`. Niech zwraca ona wartość logiczną `True`/`False` odpowiadającą na pytanie: czy długość listy `lst` jest większa lub równa od liczby `limit`. Długość listy `lst` dana jest - jak wiemy - poprzez wywołanie funkcji `len`, tj. `len(lst)`. Operator porównania daje tę wartość logiczną, np.:

```python
len( [ 1 , 2 , 3 ] ) >= 10
```

... zatem ta funkcja miałaby po prostu postać:

```python
def is_long( lst , limit ):
    return len( lst ) >= limit
```

```python
is_long( [ 1 , 2 , 3 ] , 10 )
```

Funkcja nie musi mieć żadnych argumentów! Pamiętajmy jednak oczywiście o nawiasach okrągłych - zarówno w definicji takiej funkcji, jak i w jej wywołaniu!

```python
def say_hello():
    return 'Hello!'
```

```python
say_hello()
```

    'Hello!'

 Szybkie ćwiczenie 1:

(a) Napisz funkcję, która przyjmuje jako argument temperaturę w stopniach Fahrenheita i przelicza ją na stopnie Celsjusza, zgodnie ze wzorem $C = (F - 32) \cdot 5/9$.

(b) Napisz funkcję, która oblicza wskaźnik masy ciała BMI (waga/wzrost$^2$) na podstawie wagi (w kilogramach) i wzrostu (w metrach).

(c) Niedawnym odkryciem amerykańskich naukowców jest nowy wzór przeliczający wiek psa (w latach) $d$ na odpowiadający mu wiek człowieka $h$; ma on postać: $h = 16 \ln(d) + 31$, gdzie $\ln$ to tzw. logarytm naturalny (nie przejmuj się, jeśli to nieznane pojęcie). Zaimportuj funkcję `log` z modułu `math` (implementuje ona logarytm naturalny), a następnie napisz krótką funkcję dokonującą tego przeliczenia.

(d) Napisz krótką funkcję (nazwaną powiedzmy `is_palindrome`), która zwraca wartość logiczną `True`/`False` odpowiadającą na pytanie, czy jej argument - będący stringiem - jest palindromem, a więc jest identyczny do swojego lustrzanego odbicia.

```python
# szybkie ćwiczenie 1a - rozwiązanie

```

```python
# szybkie ćwiczenie 1b - rozwiązanie

```

```python
# szybkie ćwiczenie 1c - rozwiązanie

```

```python
# szybkie ćwiczenie 1d - rozwiązanie

```

Wszystkie powyższe funkcje były "krótkie" w tym sensie, że dało się je zapisać jako jedno wyrażenie po słowie `return`. Operacje, które funkcja wykonuje, mogą jednakże być dużo bardziej skomplikowane i rozciągać się na wiele linii kodu w ciele funkcji - zob. niżej.

#### Uwaga techniczna: `def` jest stwierdzeniem wykonywalnym

`def` jest tzw. stwierdzeniem wykonywalnym ("executable statement"), co oznacza, iż funkcja zaczyna istnieć dopiero wtedy, kiedy Python przy wykonywaniu kodu ("runtime") dochodzi do miejsca z definicją `def`.

Jedną z implikacji jest to, że funkcję można do woli **redefiniować**, np.:

```python
def f( a , b ): # funkcja f zaczyna istnieć, kiedy Python dochodzi do tego miejsca w kodzie
    return a * b

print( f( 5 , 7 ) )

def f( a , b ): # kiedy Python dochodzi tutaj, tworzy inną funkcję o nazwie f - f teraz odnosi się do niej, nie do poprzedniej
    return a + b

print( f( 5 , 7 ) )
```

Inną implikacją jest to, że `def` może pojawić się w dowolnym miejscu w kodzie, gdzie jakiekolwiek inne stwierdzenie mogłoby się pojawić - np. w instrukcji warunkowej:

```python
flag = False

if flag:
    def rescale( x ):
        return 10 * x
else:
    def rescale( x ):
        return x / 10

rescale( 3 )
```

    0.3

... czy pętli. Może być też zagnieżdżone w innym stwierdzeniu `def`:

```python
def cross_out_all_proper_names( text ):

    # definicja funkcji znajdująca się w ciele innej funkcji

    def cross_out_proper_name( word ):

        if word[ 0 ].isupper():

            word_transformed = '-----'

            last_symbol = word[ -1 ]
            if not last_symbol.isalpha():
                word_transformed += last_symbol

        elif word.isnumeric():
            word_transformed = '?????'

        else:
            word_transformed = word

        return word_transformed

    # używamy tak zdefiniowanej funkcji to różnych przekształceń...

    crossed_out_words = [ cross_out_proper_name( word ) for word in text.split() ]
    crossed_out_text = ' '.join( crossed_out_words )

    return crossed_out_text
```

```python
cross_out_all_proper_names( 'Bilbo Baggins, together with Gandalf and 13 dwarves, left the Shire in the year of 2941 for a quest to the Lonely Mountain to reclaim the treasure of the dragon Smaug.' )
```

    '----- -----, together with ----- and ????? dwarves, left the ----- in the year of ????? for a quest to the ----- ----- to reclaim the treasure of the dragon -----.'

Oczywiście, możemy zadać pytanie o _sens_ robienia tego w ten sposób - tu np. nic nie stoi na przeszkodzie, aby funkcję `cross_out_proper_name` zdefiniować wcześniej, a potem tylko wywołać w ciele funkcji `cross_out_all_proper_names`. Wyobraźmy sobie jednak, że nie interesuje nas używanie funkcji `cross_out_proper_name` w innych kontekstach - wówczas "chowając" jej definicję wewnątrz funkcji `cross_out_all_proper_names` chronimy się np. przed jej ponownym użyciem gdzie indziej, co może być niepożądane. Zatem decyzję o sensie tej konstrukcji musi podjąć programista - tu pokazujemy tylko, iż jest możliwe zagnieżdżanie stwierdzeń `def`.

Z drugiej strony, `def` będące stwierdzeniem oznacza, że nie możemy napisać np. listy zawierającej elementy ze słowami `def`:
```
[ def f1(x): ... , def f2(x): ... , def f3(x): ... ] # niepoprawnie!
```
Te funkcje musimy wcześniej zdefiniować, a potem w liście umieścić tylko ich nazwy:
```
[f1, f2, f3]
```
Np.:

```python
def cm_to_in( cm ):
    return cm / 2.54

def cm_to_ft( cm ):
    return cm / 30.48

def cm_to_yards( cm ):
    return cm / 91.44

for convert_cm in [ cm_to_in , cm_to_ft , cm_to_yards ]:
    print( convert_cm( 50 ) )
```

    19.68503937007874
    1.6404199475065617
    0.5468066491688539

### 2.2. Wariacje na temat `return`

#### Funkcje zwracające wiele rezultatów jednocześnie

Nic oczywiście nie stoi na przeszkodzie, aby funkcja zwracała _kolekcję_ obiektów, a więc kilka wartości jednocześnie. Poznaliśmy już kilka rodzajów kolekcji - listy, tuple, zbiory, słowniki - i każda z nich jest dobrym sposobem, aby zwrócić wiele wartości.

Napiszmy prostą funkcję, która dla danego stringu zwraca jego pierwszą i ostatnią literę, a także jego długość - i robi to w postaci słownika:

```python
def word_stat( text ):

    first_letter = text[ 0 ]
    last_letter = text[ -1 ]
    text_len = len( text )

    return {
        'first letter' : first_letter ,
        'last letter' : last_letter ,
        'length' : text_len
    } # zwracamy kilka wartości w postaci słownika

word_stat( 'Chatham' )
```

    {'first letter': 'C', 'last letter': 'm', 'length': 7}

Najczęstszym wyborem jest jednak tupla; w składni możemy pominąć nawiasy okrągłe (pakowanie tupli):

```python
def word_stat( text ):

    first_letter = text[ 0 ]
    last_letter = text[ -1 ]
    text_len = len( text )

    return first_letter , last_letter , text_len

word_stat( 'Chatham' )
```

    ('C', 'm', 7)

... lub nawet:

```python
def word_stat( text ):
    return text[ 0 ] , text[ -1 ] , len( text )

word_stat( 'Chatham' )
```

    ('C', 'm', 7)

Chcąc przypisać wynik takiej funkcji do zmiennej, przypisujemy go do tupli tej samej długości - i również możemy pominąć nawiasy okrągłe (wypakowywanie tupli):

```python
first , last , length = word_stat( 'Chatham' )

print( first )
print( last )
print( length )
```

    C
    m
    7

Nie ma tu żadnej magii - funkcja może zwracać obiekt dowolnego typu, w szczególności tuplę czy słownik. Tupla będzie pewnie lepszym wyborem dla funkcji zwracajacych niewielką liczbę wartości, zaś słownik - gdy jest ich więcej i potrzebny jest opis tego, co która wartość znaczy.

#### Wielokrotny `return`

Stwierdzenie `return` może pojawić się nie tylko na końcu ciała funkcji - jak do tej pory - ale w dowolnym jego miejscu, a nawet wielokrotnie! Zasada jest taka, że w momencie, w którym Python _pierwszy raz_ dochodzi do któregoś stwierdzenia `return`, wykonywanie funkcji jest natychmiast przerywane i dany rezultat zwracany.

Dla przykładu, [transformację Collatza](https://en.wikipedia.org/wiki/Collatz_conjecture) (mówiliśmy o niej w Lekcji 4 przy okazji pętli `while`) możemy zapisać "klasycznie":

```python
def collatz( n ):

    if n % 2 == 0:
        n_transformed = n // 2
    else:
        n_transformed = 3 * n + 1

    return n_transformed
```

```python
collatz( 19 )
```

    58

... lub też umieszczając `return` na różnych ścieżkach instrukcji warunkowej:

```python
def collatz( n ):
    if n % 2 == 0:
        return n // 2
    else:
        return 3 * n + 1
```

```python
collatz( 19 )
```

    58

Inny przykład - napiszmy funkcję obliczającą najmniejszy dzielnik danej liczby naturalnej `n`:

```python
def min_divisor( n ):
    if type( n ) is int and n >= 2:
        for m in range( 2 , n + 1 ):
            if n % m == 0:
                return m
    else:
        return 'Error!'
```

```python
min_divisor( 187 )
```

    11

```python
min_divisor( '187' )
```

    'Error!'

#### Funkcje czyste i modyfikatory

W powyższych przykładach ciało funkcji kończy się zawsze stwierdzeniem `return`, po którym następuje wyrażenie, jakie funkcja zwraca. Nie jest to jednak konieczność - funkcja może nic nie zwracać! Co zatem nasza "maszynka" - po wrzuceniu do niej "składników" - robi? Ogólnie mówiąc, może zmieniać elementy swojego "otoczenia" - mieć tzw. **efekty uboczne** ("side effects") - np.:

- drukować jakąś wiadomość;

- modyfikować jakiś obiekt istniejący poza funkcją.

Funkcje, z jakimi mieliśmy do tej pory do czynienia, nic nie drukowały, nie zmieniały też nic w swoim otoczeniu, a po prostu produkowały jakiś rezultat - są one nazywane **funkcjami czystymi** ("pure functions"). Odwrotnie, funkcje mające efekty uboczne w postaci np. drukowania lub modyfikacji czegoś zewnętrznego to tzw. **modyfikatory** ("modifiers").

Modyfikatory mogą wprowadzać pewne zamieszanie, gdyż należy pamiętać, co i jak modyfikujemy. Niektóre języki programowania dopuszczają tylko czyste funkcje - to tzw. **programowanie funkcyjne** ("functional programming") - i są pewne dowody na to, że programowanie funkcyjne jest szybsze i mniej podatne na błędy. Modyfikatory bywają jednak użyteczne!

#### Funkcje nic nie zwracające

Najprostszym przykładem funkcji nie zwracającej żadnej wartości a mającej efekty uboczne jest funkcja, która coś drukuje:

```python
def greeting( name ):
    print( 'Hello, dear ' + name + '!' ) # funkcja greeting nie zwraca żadnej wartości, ma jedynie efekt uboczny - drukowanie

greeting( 'Basia' )
```

    Hello, dear Basia!

Zauważmy brak linijki z `return`! Tak naprawdę, nie jest to do końca prawda: formalnie rzecz biorąc, funkcja nie posiadająca stwierdzenia `return` zwraca tzw. wartość `None`. Sprawdźmy to, próbując przypisać wynik powyższej funkcji do zmiennej:

```python
g = greeting( 'Joasia' )
```

    Hello, dear Joasia!

```python
g
```

```python
g is None
```

    True

Kiedy nasza funkcja nie ma słowa `return`, Python automatycznie dodaje linijkę `return None`. Sami moglibyśmy to zrobić, aby podkreślić, że taka funkcja jednak coś zwraca - mianowicie `None`:

```python
def greeting( name ):
    print( 'Hello, dear ' + name + '!' )
    return None

greeting( 'Basia' )
```

    Hello, dear Basia!

(Moglibyśmy też po prostu napisać `return` bez niczego i efekt byłby ten sam.)

Tak napisana funkcja jest modyfikatorem - modyfikuje swoje "otoczenie" poprzez wydrukowanie wiadomości. Moglibyśmy przepisać ją w formie funkcji czystej następująco:

```python
def greeting_pure( name ):
    return 'Hello, dear ' + name + '!'
```

Funkcja ta nic nie modyfikuje, jedynie zwraca jakiś rezultat. Możemy sprawdzić przypisując wynik jej wywołania do zmiennej:

```python
p = greeting_pure( 'Basia' )

p
```

    'Hello, dear Basia!'

```python
p is None
```

    False

Dobrym pomysłem może być unikanie modyfikatorów jeśli nie ma istotnej potrzeby by ich użyć.

#### Instrukcja `pass`

Wspomnijmy w tym miejscu o słowie kluczowym `pass` - oznacza ono, "nic nie rób".

```python
def nothing():
    pass
```

```python
nothing()
```

```python
nothing() is None
```

    True

Jednym z zastosowań jest prototypowanie kodu - definiujemy funkcję, która na razie nic nie robi, dopiero później wypełnimy ją treścią. _Ciało funkcji nie może być puste, musi być wciętym blokiem kodu_, zatem jedynym sposobem, aby nic nie robiło, jest użycie `pass`.

Ta sama zasada obowiązuje dla innych wciętych bloków kodu - nie mogą być puste - np. w instrukcjach warunkowych, czy pętlach, i wtedy również możemy użyć `pass`. Np. zamiast pisać:

```python
x = 10

if x > 1000:
    print( f'{x} is large!' )
```

... moglibyśmy napisać instrukcję warunkową obejmującą wszystkie możliwe przypadki:

```python
x = 10

if x > 1000:
    print( f'{x} is large!' )
else:
    pass
```

### 2.3. Wariacje na temat argumentów

#### Przekazywanie argumentów do funkcji

Zdefiniowawszy jakąś funkcję, np.:

```python
def greeting_function( greeting , name ):
    print( f'{greeting}, {name}!' )
```

... wywołujemy ją przez **przekazanie** ("pass") jej argumentów w nawiasach okrągłych:

```python
greeting_function( 'Hello' , 'Asia' ) # chcemy, aby argument greeting miał wartość 'Hello', a name wartość 'Asia'
```

    Hello, Asia!

Mówiąc bardziej precyzyjnie:

- `greeting` i `name` w _definicji_ funkcji to tzw. **parametry**, inaczej **parametry formalne**; zachowują się one jak _zmienne_ zdefiniowane w ciele funkcji;

- kiedy zaś _wywołujemy_ funkcję, to w nawiasach okrągłych przekazujemy tzw. **argumenty**, inaczej **parametry faktyczne**, czyli obiekty, na których to konkretnie ma być wywołana funkcja; tutaj zatem argumentami są obiekty typu string, `'Hello'` i `'Asia'`.

Kiedy wywołujemy funkcję z jakimiś argumentami, następuje tzw. **powiązanie** ("binding") argumentów do parametrów, analogicznie do zwykłego przypisania obiektu do zmiennej:
```
greeting = 'Hello'
name = 'Asia'
```
Innymi słowy, w ciele funkcji utworzyliśmy zmienne lokalne `greeting` i `name`, które są teraz referencjami ("etykietami") do obiektów `'Hello'` i `'Asia'`.

#### Argumenty pozycyjne (wymagane)

Jest dość intuicyjne, że argumenty powinniśmy przekazywać do funkcji _w tej samej kolejności_, jak ustawione są parametry. Innymi słowy, powiązanie parametrów z argumentami następuje wedle kolejności. Pierwszy argument `'Hello'` jest powiązany z parametrem `greeting`, a drugi argument `'Asia'` z parametrem `name`.

Wywoławszy funkcję z inną kolejnością argumentów niż kolejność parametrów doprowadzi rzecz jasna do niepożądanych efektów:

```python
greeting_function( 'Asia' , 'Hello' )
```

    Asia, Hello!

Mówimy, że są to **argumenty pozycyjne** ("positional arguments"), jako że pozycja argumentów w wywołaniu funkcji musi odpowiadać pozycji parametrów w definicji funkcji.

Co więcej, także _liczba_ argumentów musi być dokładnie taka, jak parametrów:

```python
greeting_function( 'Hello' )
```

Z tego powodu argumenty pozycyjne nazywane są także **argumentami wymaganymi** ("required arguments").

Argumenty pozycyjne (wymagane) to najprostszy sposób przekazania argumentów do funkcji, lecz w praktyce mogący sprawiać problemy:

- Programista wywołujący funkcję musi dokładnie znać kolejność parametrów w definicji funkcji, co może prowadzić do pomyłek jeśli tych parametrów jest sporo, a sama definicja funkcji jest napisana gdzieś "daleko" (w zupełnie innym fragmencie kodu, czy też w innym module).

- Może to sprawić też problemy z czytelnością kodu z wywołaniem funkcji. Jeśli przeczytasz gdzieś `some_func(13, -1, True, 'all', 0.001)`, to nie jest prosto pamiętać, jakiemu parametrowi ma odpowiadać wartość `13`, a jakiemu `True` itd., bez dokładnego studiowania definicji funkcji. Wywoływanie funkcji z wieloma argumentami pozycyjnymi jest zupełnie nieczytelne!

#### Argumenty nazwane

Na szczęście funkcję można wywoływać za pomocą tzw. **argumentów nazwanych** ("keyword arguments"): powiązanie argumentów z parametrami nie następuje wtedy wedle kolejności, lecz poprzez składnię `parameter = argument`, czyli poprzez odwołanie się do parametrów za pomocą ich _nazwy_.

```python
greeting_function( greeting = 'Hello' , name = 'Asia' )
```

    Hello, Asia!

Wówczas kolejność w wywołaniu nie gra roli - Python wie dokładnie, jaki argument chcemy powiązać z jakim parametrem, gdyż odwołaliśmy się do nich przez nazwy:

```python
greeting_function( name = 'Asia' , greeting = 'Hello' )
```

    Hello, Asia!

Oczywiście, ciągle _liczba_ argumentów musi się zgadzać:

```python
greeting_function( name = 'Asia' )
```

Nie możemy też rzecz jasna użyć nazwy parametru, którego nie ma w definicji funkcji:

```python
greeting_function( name = 'Asia' , sincere_greeting = 'Hello' )
```

Zauważmy, jak składnia ta uwypukla fakt, iż powiązanie argumentów z parametrami jest analogiczne do świetnie nam znanego przypisania obiektów do zmiennych.

Możemy wywoływać funkcję używając obu tych metod jednocześnie, tj. zarówno z argumentami pozycyjnymi, jak i nazwanymi - z tym ograniczeniem, że wszystkie argumenty pozycyjne muszą być _wcześniej_ niż wszystkie argumenty nazwane:

```python
greeting_function( 'Hello' , name = 'Asia' )
```

    Hello, Asia!

... ale nie:

```python
greeting_function( greeting = 'Hello' , 'Asia' )
```

#### Parametry opcjonalne i domyślne wartości argumentów

Często dobrą praktyką przy pisaniu funkcji jest powiedzenie użytkownikowi czegoś w stylu: "Dla tego parametru rozsądną wartością jest to czy to." To tzw. **argument domyślny** ("default argument"), zaś parametr taki nazywamy **parametrem opcjonalnym** ("optional parameter").

- parametry opcjonalne w definicji funkcji tworzymy poprzez składnię `parameter = default_argument`;

- wówczas w wywołaniu tej funkcji można takiego argumentu już nie przekazywać (tj. pominąć go) - wówczas Python założy, że chcemy temu parametrowi nadać wartość domyślną;

- można też rzecz jasna wywołać ten parametr z inną wartością, jak zwykle.

Uwaga: Nie pomylmy argumentów domyślnych w definicji funkcji z - dyskutowanym powyżej - wywoływaniem funkcji za pomocą argumentów nazwanych! Składnia wygląda podobnie, `parameter = argument`, lecz różnica jest taka, iż:

- argumenty domyślne podajemy w _definicji_ funkcji (po `def`) jako `parameter = default_argument`;

- argumenty nazwane służą do _wywoływania_ funkcji - i w tym wywołaniu odnosimy się do nich poprzez `parameter = argument`.

Nie wszystkie parametry muszą mieć nadane wartości domyślne - może być to tylko część z nich. Zasada jest tylko taka, że w definicji funkcji wszystkie parametry z wartościami domyślnymi muszą występować _po_ parametrach bez wartości domyślnych.

Jako przykład zdefiniujmy funkcję, nadając jednemu z jej parametrów wartość domyślną (drugi nie ma wartości domyślnej - musi być w definicji funkcji występować _przed_ tym z wartością domyślną):

```python
def polite_greeting_function( name , greeting = 'Most cordially welcome' ):
    print( f'{greeting}, {name}!' )
```

... gdzie podkreślmy, że nie możemy w definicji umieścić parametrów opcjonalnych przed tymi wymaganymi:

```python
def polite_greeting_function( greeting = 'Most cordially welcome' , name ):
    print( f'{greeting}, {name}!' )
```

Możemy funkcję tę wywołać jak zwykle, podając jej oba argumenty:

```python
polite_greeting_function( 'Asia' , 'Hi' ) # wywołanie za pomocą argumentów pozycyjnych
```

    Hi, Asia!

```python
polite_greeting_function( name = 'Asia' , greeting = 'Hi' ) # wywołanie za pomocą argumentów nazwanych
```

    Hi, Asia!

Jednakże możemy też każdy z parametrów opcjonalnych _pominąć w wywołaniu_ funkcji - wtedy Python rozumie, że ma on mieć wartość domyślną:

```python
polite_greeting_function( 'Asia' ) # wywołanie za pomocą argumentów pozycyjnych
```

    Most cordially welcome, Asia!

```python
polite_greeting_function( name = 'Asia' ) # wywołanie za pomocą argumentów nazwanych
```

    Most cordially welcome, Asia!

Gdybyśmy oba parametry zdefiniowali jako opcjonalne:

```python
def polite_greeting_function( name = '... ah, what\'s your name?' , greeting = 'Most cordially welcome' ):
    print( f'{greeting}, {name}!' )
```

... to możemy rzecz jasna oba pominąć w wywołaniu:

```python
polite_greeting_function()
```

    Most cordially welcome, ... ah, what's your name?!

... lub także tylko jeden z nich:

```python
polite_greeting_function( name = 'Kasia' )
```

    Most cordially welcome, Kasia!

```python
polite_greeting_function( greeting = 'Cheers' )
```

    Cheers, ... ah, what's your name?!

Uwaga: Powyżej wywoływaliśmy tę funkcję używając składni argumentów nazwanych. Możemy też oczywiście użyć wywołania za pomocą argumentów pozycyjnych:

```python
polite_greeting_function( 'Paulina' , 'Yo' )
```

    Yo, Paulina!

... i także wtedy możemy parametry opcjonalne pomijać - jednak składnia ma teraz swoje ograniczenia, jako że wywołanie tylko z jednym argumentem odnosi go tylko do pierwszego parametru:

```python
polite_greeting_function( 'Kasia' )
```

    Most cordially welcome, Kasia!

... i nie dalibyśmy rady w ten sposób wywołać tej funkcji z parametrem `name` o wartości domyślnej, a parametrem `greeting` o wartości przekazanej w wywołaniu. Innymi słowy, bezpieczniej jest używać wywołania za pomocą argumentów nazawanych i wówczas mamy pełną kontrolę nad tym, któremu parametrowi przekazujemy wartość samodzielnie, a któremu pozostawiamy wartość domyślną.

#### Funkcje z nieznaną z góry liczbą parametrów wywoływanych pozycyjnie

Możliwa jest jeszcze następująca "magia" - funkcja przyjmująca dowolną, niezadaną z góry, liczbę argumentów! Robimy to tak: w definicji funkcji, na liście parametrów, umieszczamy parametr poprzedzony operatorem gwiazdki `*` (zob. Lekcja 7); przyjęło się, że parametr ten nazywany jest `args` (choć dowolna nazwa jest dozwolona). Np.:

```python
def my_sum( *args ):
    return sum( args )
```

Teraz:

- wywołując tę funkcję, możemy podać jej dowolną liczbę argumentów "w miejsce" `*args`,

- te argumenty zostaną spakowane do tupli `args`, dostępnej wewnątrz funkcji.

```python
my_sum( 1 , 2 , 3 ) # wywołanie z trzema argumentami
```

    6

```python
my_sum( 1 , 2 , 3 , 7 , 11 , -9 ) # wywołanie z sześcioma argumentami
```

    15

Wszystkie przekazane tak argumenty pakowane są do tupli o nazwie `args` - i teraz z tuplą tą w ciele funkcji możemy robić, co tylko z tuplą można robić! Tu np. obliczyliśmy sumę elementów tupli za pomocą `sum(args)`. Pakowanie to odbywa się poprzez operator gwiazdki (zob. Lekcja 7):

```python
*args , = 1 , 2 , 3

args
```

    [1, 2, 3]

Inny przykład: Zdefiniujmy funkcję, która będzie miała dwa zwykłe parametry wymagane, a po nich dowolną liczbę parametrów wywoływanych pozycyjnie - to jest ogólna zasada, że `*args` musi być w definicji _po_ wszystkich parametrach wymaganych. Funkcja ta niech wydrukuje numer argumentu opcjonalnego i jego wartość - iterując się przez tuplę `args` w dobrze nam znany sposób:

```python
def print_args_with_comments( header , footer , *args ): # zwykłe argumenty pozycyjne muszą być na lewo od gwiazdki

    print( header )

    for i , arg in enumerate( args ): # args to tupla, tu się przez nią iterujemy
        print( f'Argument number {i} is {arg}.' )

    print( footer )
```

```python
print_args_with_comments( 'List of arguments:' , 'That is all, folks!' , 15.3 , 'orange juice' , [ 1 , 2 , 3 ] , 0 )
```

    List of arguments:
    Argument number 0 is 15.3.
    Argument number 1 is orange juice.
    Argument number 2 is [1, 2, 3].
    Argument number 3 is 0.
    That is all, folks!

Jako kolejny przykład, przypomnijmy sobie funkcję wbudowaną `range`. Może ona przyjmować jeden, dwa lub trzy argumenty:

```python
list( range( 10 ) )
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

```python
list( range( 5 , 10 ) )
```

    [5, 6, 7, 8, 9]

```python
list( range( 5 , 10 , 2 ) )
```

    [5, 7, 9]

Spróbujmy teraz napisać samodzielnie tę funkcję - i to w ogólniejszej postaci, polegającej na tym, że każdy z argumentów może być dowolną liczbą zmiennoprzecinkową, niekoniecznie całkowitą; nasza funkcja będzie też od razu zwracać listę, bez potrzeby konwersji na listę j.w.

Skoro możemy mieć jeden, dwa albo trzy argumenty, użyjmy konstrukcji z operatorem gwiazdki - przekazujemy argumenty funkcji za pomocą `*args`. Teraz `args` w ciele funkcji jest tuplą. Sprawdzamy najpierw, czy jej długość to 1, 2 albo 3 - jeśli nie, zwracamy komunikat z błędem. Jeśli tak, to w zależności od jej długości, tworzymy zmienne `start`, `stop` i `step`, do których odpowiednio przypisujemy elementy tupli `args`, w zależności od jej długości. Np. jeśli długość tupli `args` jest 1 (czyli podaliśmy jej jeden argument), to `start` ma być równe 0, `stop` ma być równe temu jednemu argumentowu, zaś `step` ma być równe 1. Finalnie, listę wynikową konstruujemy iteracyjnie, pętlą `while`.

```python
def my_range( *args ):

    if len( args ) not in [ 1 , 2 , 3 ]:
        return 'Error! Please pass one, two or three arguments to the function.'

    else:

        # w zależności od liczby przekazanych argumentów, zdefiniujmy początek (start), koniec (stop) i krok (step) naszego przedziału

        if len( args ) == 1:
            start , stop , step = 0 , args[ 0 ] , 1
        elif len( args ) == 2:
            start , stop , step = args[ 0 ] , args[ 1 ] , 1 # lub: start , stop , step = *args , 1
        elif len( args ) == 3:
            start , stop , step = args

        # zdefiniujmy znak kroku - on będzie określał, czy idziemy "do przodu", czy w "tył"

        if step > 0:
            step_sign = 1
        elif step < 0:
            step_sign = -1
        else:
            return 'Error! Step should be non-zero.'

        # tworzymy nasz rezultat - listę range_list - iteracyjnie
        # zaczynamy od "pustego pudełka" [] i od pierwszego elementu na liście, item = start
        # w każdym kroku iteracji dodajemy item do "pudełka" i zwiększamy item o step
        # robimy to dopóki item < stop (dla kroku dodatniego) albo item > stop (dla kroku ujemnego), co łącznie można zapisać jako (stop - item) * step_sign > 0

        range_list = []
        item = start
        while ( stop - item ) * step_sign > 0:
            range_list.append( item )
            item += step

        return range_list
```

Przykłady wywołania:

```python
my_range( 10.3 )
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

```python
my_range( 5.4 , 10.5 )
```

    [5.4, 6.4, 7.4, 8.4, 9.4, 10.4]

```python
my_range( 5.4 , 10.5 , 1.5 )
```

    [5.4, 6.9, 8.4, 9.9]

```python
my_range( 10 , 5 , -1 )
```

    [10, 9, 8, 7, 6]

```python
my_range( 10.5 , 5.4 , -1.5 )
```

    [10.5, 9.0, 7.5, 6.0]

```python
my_range( 10.5 , 5.4 , 0 )
```

    'Error! Step should be non-zero.'

```python
my_range()
```

    'Error! Please pass one, two or three arguments to the function.'

```python
my_range( 1 , 2 , 3 , 4 )
```

    'Error! Please pass one, two or three arguments to the function.'

#### Funkcje z nieznaną z góry liczbą parametrów wywoływanych przez argumenty nazwane

Analogiczna konstrukcja służy do przekazania funkcji dowolnej, z góry nieznanej, liczby argumentów nazwanych.

- W definicji funkcji, na liście parametrów, umieszczamy argument poprzedzony operatorem dwóch gwiazdek `**` (zob. Lekcja 8). Jego standardowa nazwa to `kwargs` (od "keyword arguments"), choć dowolna nazwa jest dozwolona.

- W ciele funkcji zmienna `kwargs` będzie traktowana jako słownik, którego klucze odpowiadać będą nazwom parametrów, zaś wartości ich przekazanym wartościom.

- W wywołaniu funkcji, w miejsce `kwargs`, podajemy dowolną liczbę argumentów nazwanych - zgodnie z odpowiednią składnią, `parameter = argument`.

Prosty przykład takiej funkcji, która wydrukuje nazwę argumentu i jego przekazaną wartość:

```python
def print_data( title , **kwargs ):

    print( title )
    print( len( title ) * '-' )

    for key , value in kwargs.items(): # kwargs to słownik; tutaj iterujemy po jego kluczach i wartościach
        print( key + ': ' + str( value ) )
```

```python
print_data( 'Phone directory entry' , name = 'Magda' , phone = 123456 )
```

    Phone directory entry
    ---------------------
    name: Magda
    phone: 123456

```python
print_data( 'Library entry' , book = 'Lord of the Rings' , author = 'J.R.R. Tolkien' , year = 1954 , edition = '50th anniversary' , pages = 1184 )
```

    Library entry
    -------------
    book: Lord of the Rings
    author: J.R.R. Tolkien
    year: 1954
    edition: 50th anniversary
    pages: 1184

#### Wszystkie rodzaje parametrów jednocześnie

Wszystkie te sposoby mogą być stosowane jednocześnie - trzeba jedynie pilnować ich kolejności:

- najpierw znane argumenty pozycyjne/wymagane;

- następnie `*args`;

- następnie znane argumenty z wartościami domyślnymi;

- na końcu `**kwargs`.

Jako przykład, skomplikujmy trochę powyższą funkcję drukującą informacje - niech wydrukuje nam teraz pewne informacje o wpisie na blogu:

```python
def print_blog_post_data(  ):

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

#### Przestrzeń nazw

Do tej pory wszystkie nasze programy składały się z ciągu linii kodu, wykonywanych jedna po drugiej. Nawet gdy była to instrukcja warunkowa czy pętla, interpreter Pythona szedł przez nie "krok po kroku", w "ciągły" sposób. Obecność definicji funkcji w programie zmienia ten obraz diametralnie - definicja funkcji stanowi osobną tzw. **przestrzeń nazw** ("namespace"), oddzieloną od głównej części programu w "nieciągły" sposób.

Przestrzeń nazw jest to region programu, gdzie "żyją" nazwy zmiennych. Możemy mieć zmienne o tej samej nazwie, ale "żyjące" w odrębnych przestrzeniach nazw, i nie będą one ze sobą interferowały. W szczególności, w definicji funkcji możemy używać zmiennych o tych samych nazwach, co "na zewnątrz" tej definicji, i nie doprowadzi to do zamieszania. Upraszcza to pisanie kodu - pisząc funkcję, nie musisz przejmować się, że jakieś nazwy zmiennych zostały już użyte.

Mówiąc bardziej szczegółowo, zmienne zdefiniowane "na zewnątrz" jakiejkolwiek funkcji są **globalne** (to tzw. "global scope"). Są one dostępne dla wszystkich, także z ciała funkcji:

```python
x = 5

def func():
    print( x )

func()
```

    5

Jeśli zdefiniujesz zmienną o tej samej nazwie **lokalnie**, "wewnątrz" funkcji (to tzw. "local scope"), Python potraktuje je mimo to jako odrębne "etykiety".

```python
x = 5

def func( x ):
    x = 10
    print( f'x inside the function equals {x}.' )

func( x )
print( f'x outside the function equals {x}.' )
```

Okazuje się, iż możemy dostać dostęp do regionu zewnętrznego z wewnątrz funkcji - służy do tego słowo kluczowe `global`.

```python
x = 5

def func():
    global x
    x = 10
    print( f'x inside the function equals {x}.' )

func()
print( f'x outside the function equals {x}.' )
```

Czy oznacza to, że nie jesteśmy w stanie zmienić wartości argumentów podanych do funkcji poprzez operacje wewnątrz funkcji? Jesteśmy w stanie - jeśli mamy do czynienia z argumentami _mutowalnymi_, jak listy czy słowniki.

```python
def cross_out_first( lst ):
    lst[ 0 ] = '---'
    # lst = ['---', 'bar', 'baz', 'qux']

my_lst = [ 'foo' , 'bar' , 'baz' , 'qux' ]

cross_out_first( my_lst )
my_lst
```

... i zmodyfikowaliśmy argument podany do funkcji! Chodzi o to, że ciało funkcji nie próbuje przypisać do `lst` innego obiektu, ale _zmodyfikować_ (mutowalny!) obiekt, do którego referencja jest funkcji przekazana.

Jest to dobry przykład modyfikatora. Widać również, jak wiele takie funkcje mogą wprowadzić zamieszania, kiedy przestaniemy kontrolować ich efekty uboczne. Dlatego dobrym wyborem jest pisanie funkcji czystych.

```python

```

```python

```

```python

```

```python

```

```python
# late binding - at call time
```

```python
global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()
```

    bar

```python
f_list = []
for i in range(3):
    def f():
        return i
    f_list.append( f )
```

```python
f_list
```

    [<function __main__.f()>, <function __main__.f()>, <function __main__.f()>]

```python
for f in f_list:
    print( f() )
```

    2
    2
    2

```python

```

```python

```

```python

```

```python

```

```python
# list.append as a modifier; list.pop as a modifier returning value
```

```python
def my_append( lst , item ):
    lst[ len( lst ): ] = [ item ]
```

```python
my_lst = [ 1 , 2 , 3 , 4 ]

my_append( my_lst , 5 )

my_lst
```

    [1, 2, 3, 4, 5]

```python
def my_pop( lst , item_idx ):
    item = lst[ item_idx ]
    lst[ item_idx:( item_idx + 1 ) ] = []
    return item
```

```python
my_lst
```

    [1, 2, 3, 4, 5]

```python
my_pop( my_lst , 2 )
```

    3

```python
my_lst
```

    [1, 2, 4, 5]

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python
# early binding - default values
```

Default values (the right-hand i in i=i is a default value for argument name i, which is the left-hand i in i=i) are looked up at def time, not at call time, so essentially they're a way to specifically looking for early binding.

```python
f_list = []
for i in range(3):
    def f( x = i ):
        return x
    f_list.append( f )
```

```python
for f in f_list:
    print( f() )
```

    0
    1
    2

```python
def new( lst = [] ):
    lst.append( 'I\'m new here!' )
    return lst
```

```python
new()
```

    ["I'm new here!"]

```python
new()
```

    ["I'm new here!", "I'm new here!"]

```python
new()
```

    ["I'm new here!", "I'm new here!", "I'm new here!"]

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python
# first-class
```

```python
def create_empty( type_name ):
    return type_name()
```

```python
for t in [ int , float , str , list , tuple , set , dict ]:
    print( create_empty( t ) )
```

    0
    0.0

    []
    ()
    set()
    {}

```python

```

```python

```

```python

```

```python

```

```python

```

```python
# decorators
```

```python
# decorators wrap a function, modifying its behavior
```

```python
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

say_whee()
```

    Something is happening before the function is called.
    Whee!
    Something is happening after the function is called.

```python
say_whee
```

    <function __main__.my_decorator.<locals>.wrapper()>

```python
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

say_whee()
```

```python
def do_twice(func):
    def wrapper_do_twice( *args , **kwargs ):
        func( *args , **kwargs )
        func( *args , **kwargs )
    return wrapper_do_twice

@do_twice
def say_whee():
    print("Whee!")

say_whee()
```

    Whee!
    Whee!

```python
def print_function_use(func):
    def wrapper( *args , **kwargs ):
        result = func( *args , **kwargs )
        print( f'Using... {func.__name__} with {args} and {kwargs}, returning {result}.' )
        return result
    return wrapper

@print_function_use
def say_whee():
    print("Whee!")

say_whee()
```

    Whee!
    Using... say_whee with () and {}, returning None.

```python
@print_function_use
def say_hi( name ,  greet = 'Hi' ):
    print( f'{greet}, {name}.' )
```

```python
say_hi( 'Magda' , greet = 'Hello' )
```

    Hello, Magda.
    Using... say_hi with ('Magda',) and {'greet': 'Hello'}, returning None.

```python
PLUGINS = {}

def register(func):

    PLUGINS[func.__name__] = func

    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def say_hi(name):
    return f"Hi {name}"
```

```python
PLUGINS[ 'say_hi' ]('Ania')
```

    'Hi Ania'

```python
from collections import defaultdict
PLUGINS = defaultdict( int )

def register(func):

    PLUGINS[ func.__name__ ] += 1

    return func

@register
def say_hello(name):
    return f"Hello {name}"
```

```python
PLUGINS
```

    defaultdict(int, {'say_hello': 1})

```python
say_hello('A')
```

    'Hello A'

```python
def repeat( n ):
    def do_n( func ):
        def wrapper( *args , **kwargs ):
            for _ in range( n ):
                func( *args , **kwargs )
        return wrapper
    return do_n

@repeat( n = 10 )
def say_whee():
    print("Whee!")

say_whee()
```

    Whee!
    Whee!
    Whee!
    Whee!
    Whee!
    Whee!
    Whee!
    Whee!
    Whee!
    Whee!

```python
# Stateful Decorators
```

```python
# we talked about pure functions returning a value based on given arguments. Stateful decorators are quite the opposite, where the return value will depend on the current state, as well as the given arguments.
```

```python
say_whee.__dict__
```

    {}

```python
def log_function_use(func):
    def wrapper( *args , **kwargs ):
        result = func( *args , **kwargs )
        wrapper.__dict__[ 'n_calls' ] += 1
        print( 'Call ' + str( wrapper.__dict__[ 'n_calls' ] ) )
        return result
    wrapper.n_calls = 0
    return wrapper

@log_function_use
def say_whee():
    print("Whee!")

say_whee()
```

    Whee!
    Call 1

```python
say_whee.__dict__
```

    {'n_calls': 1}

```python
say_whee()
```

    Whee!
    Call 3

```python
say_whee.__dict__
```

    {'n_calls': 3}

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

## 3. Tematy zaawansowane

### 3.1. Rekursja

Seymour Papert, współtwórca języka programowania [Logo](https://en.wikipedia.org/wiki/Logo_(programming_language)), znanego wielu z nas ze szkoły podstawowej (grafika żółwia), stwierdził:

> "Of all ideas I have introduced to children, recursion stands out as the one idea that is particularly able to evoke an excited response."

**Rekursja** ("recursion") to bardzo [ogólna idea](https://en.wikipedia.org/wiki/Recursion), polegająca na tym, że **funkcja w swoim ciele odwołuje się do siebie samej**. Pierwszą myślą jest, że może spowodować to nieskończoną lawinę odwołań: funkcja woła samą siebie, ta znów woła samą siebie... i tak dalej, w nieskończoność ("infinite regress"). Ten pozorny paradoks jest źródłem tzw. "humoru rekursji". W niektórych książkach o programowaniu, indeks pojęć zawiera żartobliwy wpis:

> Recursion, _see Recursion_.

Kiedy w wyszukiwarkę Google wpiszesz hasło "recursion", żartobliwie podpowiada ona:

> Did you mean: _recursion_

Pewna osoba zapytana na rozmowie rekrutacyjnej o zdefiniowanie rekursji, wzięła kartkę papieru i napisała po jej obu stronach:

> Please turn over.

Innym przykładem są tzw. ["rekursywne akronimy"](https://en.wikipedia.org/wiki/Recursive_acronym), np. nazwa popularnego języka programowania PHP, pierwotnie oznaczająca "Personal Home Page", teraz tłumaczona jest jako "PHP: Hypertext Preprocessor".

Jest wiele innych kontekstów rekursji, np. w geometrii (fraktale - ich coraz mniejsze części wyglądają jak całość), czy sztuce (np. litografia Eschera "Print Gallery", technika ["Mise en abyme"](https://en.wikipedia.org/wiki/Mise_en_abyme)).

W praktyce unikamy nieskończonego regresu poprzez umieszczenie odpowiedniego warunku brzegowego. Musimy mieć zatem dwa komponenty:

- Prosty, podstawowy, **warunek brzegowy** - krańcowy scenariusz, który _nie_ używa rekursji w swojej definicji.

- **Krok rekursywny** - odwołanie funkcji do samej siebie, ale w taki sposób, który przybliża nas coraz bardziej do warunku brzegowego.

Klasycznym przykładem definicji rekursywnej jest dobrze nam znany ciąg Fibonacciego, dla którego:

- warunkiem brzegowym jest $F_0 = 0$, $F_1 = 1$;

- krokiem rekursywnym jest wołanie samego siebie, $F_n = F_{n - 1} + F_{n - 2}$, dla $n \geq 2$.

Widzimy tu zatem, iż chociaż mamy to odwołanie do samego siebie, to krok rekursywny prowadzi coraz bliżej warunku brzegowego, gdzie rekursja się zatrzymuje.

W informatyce słowo "rekursja" ma zwykle nieco bardziej ograniczone znaczenie i dotyczy _funkcji_, które w swoim ciele wołają same siebie. Jako jeszcze prostszy niż ciąg Fibonacciego przykład rekursji rozważmy obliczenie sumy liczb od 0 do `n`. Możemy oczywiście zrobić to funkcją wbudowaną:

```python
sum( range( 101 ) )
```

    5050

... ale spróbujmy sami zdefiniować tę sumę rekurencyjnie. Jeśli zdefiniujemy $S_n = 0 + 1 + 2 + \ldots + n$, to widzimy, iż:

- krok rekurencyjny to $S_n = n + S_{n - 1}$,

- warunek brzegowy to $S_0 = 0$.

Zakodujmy to poznaną składnią `def`:

```python
def sum_recursion( n ):
    if n == 0:
        return 0
    else:
        return n + sum_recursion( n - 1 )
```

```python
sum_recursion( 100 )
```

    5050

 Szybkie ćwiczenie 2: Silnia.

Innym klasycznym przykładem definicji rekursywnej jest silnia, $n! = 1 \cdot 2 \cdot \ldots \cdot n$, czyli iloczyn kolejnych liczb naturalnych. Możemy tę definicję wyrazić jako:

- krok rekursywny: $n! = n \cdot (n - 1)!$;

- warunek brzegowy: $1! = 1$.

Napisz rekursywną definicję tej funkcji (nazwijmy ją `factorial_recursion`).

```python
# szybkie ćwiczenie 2 - rozwiązanie

```

 Szybkie ćwiczenie 3: Funkcja Ackermanna.

Bardzo ciekawym przykładem funkcji zdefiniowanej rekursywnie jest [funkcja Ackermanna](https://en.wikipedia.org/wiki/Ackermann_function). (Jest to najprostszy znany przykład funkcji, która nie jest "pierwotnie rekursywna", co z grubsza oznacza, iż nie da jej się zapisać wyłącznie za pomocą pętli `for`.)
\begin{equation}
A( m , n ) =
    \begin{cases}
        n + 1 & \text{jeśli $m = 0$}\\
        A( m - 1 , 1 ) & \text{jeśli $m > 0$ i $n = 0$}\\
        A( m - 1 , A( m , n - 1 ) ) & \text{jeśli $m > 0$ i $n > 0$}
    \end{cases}
\end{equation}

Napisz rekursywną definicję tej funkcji (nazwijmy ją `ackermann`). Jej wartości rosną w niezwykle szybkim tempie, podobnie jak liczba wywołań potrzebnych do dojścia do warunku brzegowego, więc nie próbuj wywoływać już nawet `ackermann(4, 1)`. Natomiast `ackermann(3, 1)` jest równe 13.

```python
# szybkie ćwiczenie 3 - rozwiązanie

```

 Szybkie ćwiczenie 4: Największy wspólny dzielnik.

Obliczenie największego wspólnego dzielnika (GCD = "greatest common divisor") dwóch liczb naturalnych `m` i `n` (czyli największej takiej liczby, przez którą `m` i `n` obie dzielą się bez reszty) można dokonać tzw. [algorytmem Euklidesa](https://en.wikipedia.org/wiki/Euclidean_algorithm):
\begin{equation}
GCD( m , n ) =
    \begin{cases}
        m & \text{jeśli $n = 0$}\\
        GCD( n , m \% n ) & \text{jeśli $n > 0$}
    \end{cases}
\end{equation}
... gdzie `m % n` to reszta z dzielenia `m` przez `n`. Zdefiniuj tę funkcję `GCD` rekursywnie.

Sprawdź swoją odpowiedź: GCD liczb 1386 i 3213 wynosi 63.

```python
# szybkie ćwiczenie 4 - rozwiązanie

```

Na koniec pewna uwaga praktyczna: Zdefiniujmy najpierw rekursywnie ciąg Fibonacciego, ale dodajmy do niego instrukcję `print`, sprawdzającą, ile razy funkcja wywołała samą siebie:

```python
def fibonacci_recursion( n ):
    print( f'Calling F({n}).' )

    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci_recursion( n - 1 ) + fibonacci_recursion( n - 2 )
```

```python
fibonacci_recursion( 5 )
```

    Calling F(5).
    Calling F(4).
    Calling F(3).
    Calling F(2).
    Calling F(1).
    Calling F(0).
    Calling F(1).
    Calling F(2).
    Calling F(1).
    Calling F(0).
    Calling F(3).
    Calling F(2).
    Calling F(1).
    Calling F(0).
    Calling F(1).

    5

Problem z tą implementacją jest taki, że niepotrzebnie wielokrotnie obliczamy te same elementy ciągu. Rozwiązaniem jest przechowywanie raz obliczonych elementów ciągu w pamięci tymczasowej ("cache"), a następnie jedynie odwoływanie się do nich w razie potrzeby. Można dokonać tego następującym kodem - którego na razie nie tłumaczmy! `lru_cache` to tzw. **dekorator**.

```python
from functools import lru_cache

@lru_cache( maxsize = None )
def fibonacci_recursion( n ):
    print( f'Calling F({n}).' )

    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci_recursion( n - 1 ) + fibonacci_recursion( n - 2 )

fibonacci_recursion( 5 )
```

    Calling F(5).
    Calling F(4).
    Calling F(3).
    Calling F(2).
    Calling F(1).
    Calling F(0).

    5

### 3.2. Funkcje anonimowe (`lambda`)

Mamy już sporo doświadczenia z definiowaniem funkcji za pomocą słowa kluczowego `def`. Konstrukcja ta ma nieograniczone możliwości - dowolne zachowanie jesteśmy w stanie wyabstrahować w postaci funkcji zdefiniowanej przez `def`. W tej sekcji zajemiemy się drugą metodą definicji funkcji, tzw. **funkcjach anonimowych**, znanych też jako funkcje `lambda`. (Wspominaliśmy już o nich w materiałach dodatkowych do Lekcji 6, na temat technik `reduce`, `map`, `filter`. Spotkaliśmy je też przy okazji omawiania typu `defaultdict` w Lekcji 8.)

Najpierw składnia: są one definiowane przy użyciu słowa kluczowego `lambda`, a nie `def`, w postaci:
```
lambda arguments : expression
```

Są to generalnie takie same "maszynki", które przyjmują argumenty `arguments` i zwracają wyrażenie `expression` - z trzema ogólnymi różnicami wobec "pełnoprawnych funkcji":

- powinny być "krótkie" - rezultat jest ograniczony do jednego wyrażenia `expression`, a więc operacja, którą definiują, musi dać się "krótko" zapisać (choć jak widzieliśmy, nawet dość skomplikowane operacje da się tak zapisać, choćby przy użyciu "list comprehension");

- są **anonimowe** ("anonymous") - nie mają nazwy (można je przypisać do zmiennej, ale to nie jest nazwa funkcji!);

- z formalnego punktu widzenia, `lambda` to wyrażenie ("expression"), a nie stwierdzenie ("statement") jak `def` - oznacza to, iż można je umieszczać w miejscach niedozwolonych dla stwierdzeń, np. jako argument przy wywoływaniu innej funkcji, element listy itd.

Funkcje `lambda` są bardzo użyteczne w różnych kontekstach, gdzie liczy się szybka, zwięzła operacja funkcyjna.

Np. prosta funkcja matematyczna z początku tej lekcji w formie `lambda`, przypisana do zmiennej o nazwie `fun`:

```python
fun = lambda x : 2 * x + 5

fun( 7 )
```

    19

Funkcje `lambda` powinny być "krótkie", natomiast jedno wyrażenie może robić więcej niż się wydaje! Z drugiej strony, jeśli chcemy tworzyć naprawdę złożone funkcje, zwykle dobrym pomysłem jest jednak użycie `def` - funkcje `lambda` zaprojektowane są z myślą o krótkich fragmentach kodu wykonywanego w tej samej linijce, w której są wstawione. Tu np. funkcja `lambda` z dwoma argumentami, listą liczb `amounts` i stringiem `currency`, zwracająca listę, gdzie każda z liczb zmieniona jest na string wypisujący tę liczbę, z dodanym po spacji słówkiem `currency`:

```python
curr = lambda amounts , currency : [ str( amount ) + ' ' + currency for amount in amounts ]

curr( [ 1.5e7 , 2.3e8 , 7.8e6 , -3.9e7 ] , 'USD' )
```

    ['15000000.0 USD', '230000000.0 USD', '7800000.0 USD', '-39000000.0 USD']

Inny przykład:

```python
is_even = lambda n : n % 2 == 0

is_even( 8 )
```

    True

Funkcja `lambda` może też nie przyjmować żadnych argumentów, np.:

```python
always = lambda : 'Always there!'

always()
```

    'Always there!'

... a funkcje takie są przydatne np. przy tworzeniu słowników `defaultdict`.

#### Przykład: Konstrukcja "switch"

Funkcje `lambda` można umieszczać w miejscach niedozwolonych dla stwierdzeń `def`, np. jako elementy list czy słowników. Wyobraźmy sobie np. "tabelę akcji" (tzw. **"switch"**), czyli słownik, w którym klucze to różne przypadki, a wartości to proste funkcje ("akcje"), wykonywane w danym przypadku:

```python
action = {
    'case A' : lambda x : x + 5 ,
    'case B' : lambda x : 2 * x - 3 ,
    'case C' : lambda x : x ** 2 + 1 ,
    'case D' : lambda x : abs( x ) - 7
}

action[ 'case C' ]( 3.64 ) # action[ 'case C' ] jest funkcją, którą wywołujemy z argumentem o wartości 3.64
```

W ten sposób możemy zdefiniować paletę prostych zachowań zależnych np. od decyzji użytkownika, np.:

```python
from math import pi

cylinder = {
    'total volume' : lambda r , h : pi * r ** 2 * h ,
    'total surface' : lambda r , h : 2 * pi * r * h + 2 * pi * r ** 2 ,
    'side surface' : lambda r , h : 2 * pi * r * h
}

r = float( input( 'Enter cylinder\'s radius:' ) )
h = float( input( 'Enter cylinder\'s height:' ) )
what = input( 'What would you like to calculate (total volume, total surface, side surface):' )

print( f'{what.capitalize()} of your cylinder is {cylinder[ what ]( r , h )}.' )
```

#### Przykład: Sortowanie według klucza

Inne przydatne zastosowanie funkcji `lambda` ma związek z **sortowaniem**. Rozważmy kolekcję obiektów, dla których zdefiniowane jest pojęcie "porządku" (mniejszy/większy), np. liczb czy stringów (porządek leksykograficzny). Możemy je posortować funkcją wbudowaną `sorted`:

```python
sorted( [ 5 , 2 , 4 , 1 , 3 ] )
```

```python
sorted( [ 'ABCdef' , 'abcde' , 'aBcd' , 'Abc' ] ) # wielkie litery są "mniejsze" od małych liter
```

```python
sorted( 'unununium' )
```

```python
sorted( { 2 : 'a' , 1 : 'b' } ) # sortuje klucze słownika
```

Funkcja `sorted` nie modyfikuje oryginalnej kolekcji, lecz zwraca nową kolekcję, z posortowanymi elementami. Typ `list` ma także analogiczną metodę `sort`, która sortuje listę "w miejscu", np.:

```python
lst = [ 3 , 2 , 1 ]

lst.sort()

lst
```

... ale skupmy się jednak na funkcji `sorted`; jest ona choćby ogólniejsza od metody `sort`, jako że ta jest metodą stricte list, natomiast funkcja `sorted` działa na ogólniejszych kolekcjach.

Funkcja `sorted` ma opcjonalny argument nazwany `reverse`, z wartością domyślną `False`; kiedy ustawi się ją na `True`, sortowanie przebiega od największego do najmniejszego:

```python
sorted( [ 5 , 2 , 4 , 1 , 3 ] , reverse = True )
```

Ma ona też drugi opcjonalny argument nazwany, `key`, na którym to właśnie się skupmy. Argument ten powinien być funkcją o jednym argumencie, która zwraca "klucz", po którym będziemy sortować. Klasycznym przykładem jest sortowanie listy tupli, gdzie możemy zechcieć np. sortować po drugim elemencie:

```python
kids = [ ( 'Ania' , 15 ) , ( 'Basia' , 11 ) , ( 'Ela' , 13 ) ]

sorted( kids , key = lambda x : x[ 1 ] )
```

Funkcja przekazana przez argument `key` robi co następuje:

- Jest ona aplikowana do każdego elementu kolekcji. Tutaj mamy listę tupli 2-elementowych `kids`. Funkcja podana w argumencie `key` jest aplikowana do wszystkich tych tupli po kolei. Jak widzimy, dla każdej takiej tupli zwraca jej drugi element. Zatem efektem zaaplikowania tej funkcji do wszystich elementów będzie lista `[15, 11, 13]`.

- Dopiero tak otrzymaną listę sortujemy. Tutaj dostajemy `[11, 13, 15]`, co przekłada się na porządek oryginalnej listy `
[('Basia', 11), ('Ela', 13), ('Ania', 15)]`.

Często w tym miejscu stosuje się funkcję `lambda` j.w. Nie musi tak jednak być. Oto przykład, gdzie sortujemy listę stringów, ale nie biorąc pod uwagę wielkości liter (jak pamiętamy, wielkie litery są "mniejsze" od małych - tu chcemy tę własność zaniedbać):

```python
sorted( [ 'ABCdef' , 'abcde' , 'aBcd' , 'Abc' ] , key = str.lower )
```

Argumentem `key` jest tu funkcja z jednym argumentem, `str.lower`, zamieniająca wielkie litery na małe (`lower` to metoda stringów, ale pamiętamy, iż metody to też funkcje, tylko trzeba je odpowiednio nazwać: nazwa typu, kropka, nazwa metody). Ona zatem najpierw konwertuje naszą listę na `[ 'abcdef' , 'abcde' , 'abcd' , 'abc' ]` i to ją sortuje, `
['abc', 'abcd', 'abcde', 'abcdef']`, co przekłada się na powyższą postać listy oryginalnej.

Inny przykład: Rozważmy listę:

```python
list( range( -5 , 6 ) )
```

... i posortujmy ją według klucza będącego wartością bezwzględną:

```python
sorted( range( -5 , 6 ) , key = abs )
```

Co tu się stało? Funkcja z jednym argumentem `abs` zaaplikowana do każdego elementu oryginalnej listy dała listę `[5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5]`. To tę listę sortujemy, co przekłada się na powyższy wynik.

Pojawia się przy okazji ciekawe spostrzeżenie: w tej przekonwertowanej liście są elementy o _tej samej wartości_. Jak zatem są one sortowane? Odpowiedź na to pytanie pojawia się w dokumentacji Pythona: sortowania mają zagwarantowaną **stabilność**. [Stabilność algorytmu sortującego](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability) oznacza, iż elementy o tym samym kluczu pozostawione są w oryginalnej kolejności. Zatem np. skoro elementy -1 i 1 oryginalnej listy mają ten sam klucz (tj. tę samą wartość 1 po przekonwertowaniu funkcją `abs`), to pozostawione zostają w oryginalnej kolejności, czyli -1 przed 1.

 Szybkie ćwiczenie 5: Jak zmodyfikować powyższy przykład, aby po posortowaniu dostać listę `[0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5]`, gdzie -1 jest _po_ 1 itd.?

Wskazówka: Wykorzystaj stabilność sortowania i odpowiednio zmodyfikuj oryginalną listę.

```python
# szybkie ćwiczenie 5 - rozwiązanie

```

 Szybkie ćwiczenie 6: Masz daną listę `numerals`, jak niżej.

(a) Posortuj ją według długości słów, od najdłuższego do najkrótszego.

(b) Posortuj ją według ostatniej litery (w normalnym porządku leksykograficznym).

(c) Posortuj ją według liczby wystąpień litery `'e'` w nich.

```python
numerals = [ 'zero' , 'one' , 'two' , 'three' , 'four' , 'five' , 'six' , 'seven' , 'eight' , 'nine' ]
```

```python
# szybkie ćwiczenie 6a - rozwiązanie

```

```python
# szybkie ćwiczenie 6b - rozwiązanie

```

```python
# szybkie ćwiczenie 6c - rozwiązanie

```

 Szybkie ćwiczenie 7: Masz daną listę `origami_masters` imion i nazwisk; masz także osobny słownik `birthdays`, którego kluczami są te imiona, a wartościami stringi z datami urodzin.

(a) Posortuj listę `origami_masters` według nazwisk.

(b) Posortuj listę `origami_masters` według dat urodzenia, zapisanych w słowniku `birthdays`.

```python
origami_masters = [
    'Toshikazu Kawasaki' ,
    'Satoshi Kamiya' ,
    'Akira Yoshizawa' ,
    'Éric Joisel' ,
    'Robert J. Lang' ,
    'Nick Robinson' ,
    'Sipho Mabona'
]

birthdays = {
    'Toshikazu Kawasaki' : '1955-11-26' ,
    'Satoshi Kamiya' : '1981-06-06' ,
    'Akira Yoshizawa' : '1911-03-14' ,
    'Éric Joisel' : '1956-11-15' ,
    'Robert J. Lang' : '1961-05-04' ,
    'Nick Robinson' : '1957-01-08' ,
    'Sipho Mabona' : '1980-01-11'
}
```

```python
# szybkie ćwiczenie 7a - rozwiązanie

```

```python
# szybkie ćwiczenie 7b - rozwiązanie

```

### 3.3. Typowanie

#### "Duck typing"

Jedną z pięknych i bardzo użytecznych cech Pythona jest tzw. **"duck typing"**. Oznacza to ogólnie, że dana operacja (funkcja, metoda, operator...) może przyjąć jako argument obiekt _dowolnego typu_, jeśli tylko jest ona dla tego typu zdefiniowana. Innymi słowy, ta sama operacja może być zdefiniowana dla obiektów różnych typów, dla każdego typu dając inny efekt - specyficzny dla tego typu.

Klasycznym przykładem w Pythonie jest operator `+`, który dla liczb (typy `int` i `float`) oznacza dodawanie arytmetyczne:

```python
9 + 16
```

Ten sam operator `+` zastosowany jednak do dwóch list ma zupełnie inne znaczenie - łączy te listy ze sobą:

```python
[ 'apple' , 'orange' , 'pear' ] + [ 'potato' , 'cabbage' ]
```

... a zastosowany do stringów, łączy je w jeden string:

```python
'Jan' + ' ' + 'Kowalski'
```

Typ danych obiektu jest zatem mniej istotny od zdefiniowanych dla niego operacji. Skoro `+` jest zdefiniowany dla typów `int`, `float`, `list`, `str`, to pisząc wyrażenie z tym operatorem nie musimy sprawdzać typów - wystarczy wiedzieć, że operacja jest dla tych typów zdefiniowana.

Nazwa "duck typing" pochodzi od tzw. ["testu kaczki"](https://en.wikipedia.org/wiki/Duck_test) i powiedzenia:

> "If it walks like a duck, and it quacks like a duck, then it must be a duck."

Nie powinniśmy martwić się zatem, czy obiekt "jest kaczką" (czy jest liczbą, listą, stringiem...), a jedynie o to, czy "kwacze" (czy umie stosować `+`). Cokolwiek, co "kwacze" się nadaje, nawet jeśli nie jest "kaczką".

Logika tu jest taka, iż wspomniane obiekty - liczby, listy, stringi - "wiedzą", co to jest `+`; w definicji tych obiektów zawarta została definicja operatora `+`, opisująca, co on na obiektach tych robi. "Duck typing" działa automatycznie, jeśli tylko użyte obiekty "wiedzą" o istnieniu danej operacji.

```python
8 + [ 2 ] # operator + nie jest zdefiniowany między liczbą a listą!
```

Innym klasycznym przykładem jest funkcja wbudowana `len`, którą można wywołać np. na stringach, listach, tuplach, zbiorach, słownikach.

To samo oczywiście stosuje się do naszych własnych funkcji. Zdefiniujmy np. funkcję, która przyjmuje dwa argumenty - w zamyśle listy - i zwraca ich elementy wspólne:

```python
def intersect( seq_1 , seq_2 ):
    return [ item for item in seq_1 if item in seq_2 ]
```

Działa ona oczywiście na listach:

```python
intersect( [ 5 , 8 , -3 , 11 , 9 ] , [ 9 , 4 , 5 ] )
```

... ale po przyjrzeniu się jej treści widzimy, że równie dobrze możemy zastosować ją np. do dwóch stringów, ponieważ stringi "wiedzą", co to pętla `for` i co to operator `in`:

```python
intersect( 'monkey' , 'donkey' )
```

Jeden argument może być listą, drugi stringiem, nie ma problemu:

```python
digraphs = [ 'ch' , 'cz' , 'dz' , 'dź' , 'dż' , 'rz' , 'sz' ]
text = 'w szczebrzeszynie chrząszcz brzmi w trzcinie'

intersect( digraphs , text )
```

#### Adnotacje

"Duck typing" jest niezwykle wygodny, lecz może być podatny na błędy, jeśli zechcemy zastosować daną operację na typie danych, który o niej "nie wie". Pewną pomocą w tej kwestii są tzw. **adnotacje** ("annotations", "type hints"), które umieszczamy w definicji funkcji w następujący sposób:

```python
def intersect( seq_1 : list , seq_2 : list ) -> list:
    return [ item for item in seq_1 if item in seq_2 ]
```

Adnotacje możemy dodać po argumentach funkcji, po dwukropku `:`, i oznaczają one wtedy, że _sugerowanym_ typem argumentu jest ten podany po nim. Podobnie możemy zasugerować typ obiektu zwracanego przez funkcję, po symbolu `->` w nagłówku. Są to jednak cały czas tylko sugestie, w żaden sposób nie wymuszane w czasie wykonywania programu; możemy np. nadal wykonać:

```python
intersect( digraphs , text )
```

Można sprawić, aby Python statycznie sprawdzał adnotacje i wyrzucał błąd jeśli otrzymane argumenty nie będą takiego typu, jak zasugerowany - służy do tego [zewnętrzna biblioteka `mypy`](http://mypy-lang.org/). Ale nawet i bez takiego rygoru adnotacje mogą pomóc utrzymać porządek w kodzie - choć na pewno ich pisanie dodaje pracy; są one [szczególnie pomocne](https://www.bernat.tech/the-state-of-type-hints-in-python/) w większych projektach, nad którymi pracuje więcej osób.

Dodajmy jeszcze, że adnotacje danej funkcji możemy sprawdzić przez jej atrybut `__annotations__`:

```python
intersect.__annotations__
```

Temat sprawdzania typów w Pythonie jest [dość złożony](https://realpython.com/python-type-checking/) i bardzo techniczny, poza ramami tego kursu.

#### "Docstrings"

Rozpowszechnionym sposobem na poprawę czytelności programów są tzw. **"docstrings"**, czyli opisy działania funkcji skonstruowane wedle [ściśle określonych zasad](https://www.python.org/dev/peps/pep-0257/) (choć są różne warianty, m.in. "Sphinx Style", "Google Style", "Numpy Style"). Podstawowe zasady to:

- są to wielolinijkowe stringi, zamknięte w potrójnych cudzysłowach `"`;

- "docstring" zaczyna się od razu po nagłówku funkcji;

- jego pierwsza linijka stanowi krótkie podsumowanie działania funkcji, zaś po nim mamy linijkę przerwy;

- następnie mamy dłuższy opis działania funkcji;

- na końcu opisujemy pokrótce każdy z argumentów i rezultat zwracany przez funkcję;

- ostatnia linijka "docstring" jest pusta.

```python
def intersect( seq_1 , seq_2 ):
    """
    Intersection of two lists.

    Iterates through the first list and retains only the elements present in the second list.

    Parameters:
    seq_1 (list): the first list
    seq_2 (list): the second list

    Returns:
    list: A list containing the common elements of seq_1 and seq_2.

    """
    return [ item for item in seq_1 if item in seq_2 ]
```

"Docstring" danej funkcji dostępny jest przez atrybut `__doc__`:

```python
print( intersect.__doc__ )
```

### 3.4. Funkcje to obiekty pierwszej klasy

Widzieliśmy już na różnych przykładach, że z funkcjami w Pythonie można "wszystko robić", tj.:

- można przypisywać funkcję do zmiennej;

- funkcje można przechowywać w różnych strukturach danych, jak listy, słowniki itd.;

- funkcja może być podana jako argument do innej funkcji;

- funkcja może zwracać inną funkcję.

Obiekty, z którymi można robić te fundamentalne czynności, nazywane są **obiektami pierwszej klasy** ("first-class objects") - przykłady to liczby, stringi, listy - a więc funkcje to w Pythonie również obiekty pierwszej klasy.

#### Przypisywanie funkcji do zmiennej

Zdefiniujmy prostą funkcję jako przykład:

```python
def shout( text ):
    return text.upper()
```

Przypisujemy ją do zmiennej `yell`:

```python
yell = shout
```

... i teraz zmienna ta wskazuje na funkcję `shout`:

```python
yell( 'hello!' )
```

Nazwę funkcji możemy w Pythonie otrzymać atrybutem `__name__`:

```python
shout.__name__
```

... i mamy:

```python
yell.__name__
```

Zmienna wskazująca na funkcję i sama ta funkcja to dwie odrębne rzeczy.

Spotkaliśmy się też już z przypisywaniem funkcji `lambda` do zmiennej.

#### Przechowywanie funkcji w strukturach danych

Funkcje mogą być np. elementami kolekcji. Widzieliśmy już wcześniej słownik funkcji przy okazji konstrukcji "switch". Tutaj inny przykład, z listą funkcji:

```python
format_options = [
    yell ,
    str.lower ,
    str.capitalize
]

for format_option in format_options:
    print( format_option( 'Hello World!' ) )
```

#### Przekazywanie funkcji jako argument innej funkcji

Napiszmy funkcję `greet`, która wydrukuje tekst powitania, ale przekażmy jej jako argument inną funkcję, która zdefiniuje nam sposób formatowania tego powitania:

```python
def greet( format_option ):
    return format_option( 'Hello World!' )
```

Teraz wywołajmy funkcję `greet` z różnymi wartościami argumentu, tj. różnymi funkcjami:

```python
greet( yell )
```

```python
greet( str.lower )
```

```python
greet( lambda text : text[ ::2 ] )
```

... itd. Jest to dość niesamowite: funkcji możemy jako argument przekazywać "zachowanie", w formie funkcji opisującej to "zachowanie". Podobny przykład widzieliśmy przy okazji argumentu `key` funkcji wbudowanej `sorted`.

#### Funkcja zwracająca funkcję

Wspomnieliśmy już wcześniej, że funkcję możemy zdefiniować wewnątrz definicji innej funkcji. Co więcej, możemy taką funkcję zwrócić jako rezultat - innymi słowy, funkcje mogą zwracać "zachowanie".

```python
def speak( how ):

    def scream( text ):
        return text.upper() + '!!!'

    def murmur( text ):
        return '...' + text.lower() + '...'

    def just_say_it( text ):
        return text

    if how == 'scream':
        return scream
    elif how == 'murmur':
        return murmur
    else:
        return just_say_it
```

Teraz np.:

```python
speak( 'scream' )
```

... jest funkcją, którą możemy wywołać na tekście:

```python
speak( 'scream' )( 'Hello world' )
```

Zastosowaniem tej możliwości jest tzw. "fabryka funkcji", np.:

```python
def generate_power( n ):

    def nth_power( x ):
        return x ** n

    return nth_power
```

```python
pow_3 = generate_power( 3 )

pow_3
```

```python
pow_3( 2 )
```

Funkcje przyjmujące jako argumenty inne funkcje, bądź zwracające inne funkcje, nazywane są **funkcjami wyższego rzędu** ("higher-order functions").

## 4. Zadania domowe

### 4.1. Dłuższe ćwiczenia

 Dłuższe ćwiczenie 8: Wzór Herona.

[Wzór Herona](https://pl.wikipedia.org/wiki/Wz%C3%B3r_Herona) pozwala obliczyć pole trójkąta na podstawie długości jego boków, `a`, `b` i `c`:

$S = \sqrt{p(p - a)(p - b)(p - c)} , \quad p = (a + b + c)/2$

Napisz funkcję implementującą tę formułę. Uwzględnij sytuację, kiedy wyrażenie pod pierwiastkiem kwadratowym jest ujemne - wówczas odcinki `a`, `b`, `c` mają długości niepozwalające na utworzenie trójkąta; w tym wypadku wydrukuj odpowiedni komunikat, np. "It's not a triangle!". Odnajdź i zaimportuj funkcję pierwiastka kwadratowego z modułu `math`.

Sprawdź swoją odpowiedź: Pole trójkąta o bokach 3, 4, 5 wynosi 6.

```python
# dłuższe ćwiczenie 8 - rozwiązanie

```

 Dłuższe ćwiczenie 9 (\*): Całka określona.

Napisz funkcję (o nazwie `integrate`), przyjmującą cztery argumenty: pierwszym argumentem jest _inna funkcja_ `f` (ona będzie miała jeden argument liczbowy), kolejne dwa to liczby zmienno-przecinkowe `a`, `b`, a ostatnim dodatnia liczba całkowita `n`.

Funkcja ta dokona przybliżonego obliczenia tzw. całki określonej funkcji $f$ między punktami $a$ i $b$, tj. $\int_{a}^{b} f(x)dx$. Pojęcie całki jest co prawda dość zaawansowane, ale sama jej definicja jest bardzo prosta! Jest to pole powierzchni pod wykresem funkcji $f$, a między pionowymi liniami postawionymi w punktach $a$ i $b$.

Przybliżone jej obliczenie polega na narysowaniu serii $n$ wąskich prostokątów i zsumowanie ich pól powierzchni (jest to tzw. "suma Riemanna"). Podstawa każdego prostokąta ma długość $d/n$, gdzie zdefiniujmy $d = b - a$, gdyż odcinek między $a$ i $b$ dzielimy na $n$ równych części. Podział ten jest dokonany w następujących punktach:
$$
x_0 = a, \quad x_1 = a + \frac{d}{n}, \quad x_2 = a + \frac{2d}{n}, \quad \ldots \quad x_n = a + n \cdot \frac{d}{n} = b
$$
Wysokość natomiast kolejnych prostokątów to $f(x_0)$, $f(x_1)$, ..., $f(x_{n - 1})$. Skoro tak, to pole powierzchni każdego prostokąta to kolejno $f(x_0) \cdot d/n$, $f(x_1) \cdot d/n$, ..., $f(x_{n - 1}) \cdot d/n$. Zatem przybliżona wartość całki określonej to suma tych pól powierzchni:
$$
\frac{d}{n} \left( f( x_0 ) + f( x_1 ) + \ldots + f( x_{n - 1} ) \right)
$$
Oczywiście, im liczba $n$ większa, tym dokładniejsze jest przybliżenie. Możesz używać wartości np. 10 000.

Napisz tę funkcję najpierw krok po kroku, jak to tu opisano, definiując kolejno zmienne `d`, `x_list` (lista punktów $x_0$, ..., $x_{n - 1}$), `f_list` (listę wartości funkcji $f$ w tych punktach) i wreszcie wynik `riemann_sum`. Następnie spróbuj napisać ją w jednej linijce, po słowie `return`, za pomocą "list comprehension".

Sprawdź swoje rozwiązanie: Całka określona funkcji $f(x) = x^2 \sin(x)^3$ między 0 a 3 wynosi ok. 3.6159. Wywołaj swoją funkcję `integrate` z tymi wartościami argumentów, gdzie funkcję `f` zapisz jako funkcję `lambda` bezpośrednio w wywołaniu funkcji `integrate` (potrzebna będzie ci też funkcja sinus z modułu `math`).

```python
# dłuższe ćwiczenie 9 - rozwiązanie dłuższe

```

```python
# dłuższe ćwiczenie 9 - rozwiązanie krótsze

```

```python
# dłuższe ćwiczenie 9 - sprawdź swoje rozwiązanie

```

 Dłuższe ćwiczenie 10: Szyfr Cezara.

[Szyfr Cezara](https://en.wikipedia.org/wiki/Caesar_cipher) to jedna z najstarszych znanych metod szyfrowania; od Swetoniusza wiemy, że używał go Juliusz Cezar do szyfrowania listów do swoich przyjaciół. Polega on na tym, że każdą literę w alfabecie przesuwamy o daną liczbę miejsc, np. o 3, czyli np. `'a'` dałoby `'d'`, zaś `'y'` dałoby `'b'` itd. Aby zakodować dany tekst, przesuwamy każdą jego literę o 3 w przód; aby zakodowany tekst odkodować, przesuwamy jego litery o 3 w tył.

Napisz funkcję `ceasar_shift`, która przyjmie dwa argumenty: literę `letter` i liczbę całkowitą `shift`, a zwróci literę po odpowiednim przesunięciu. Przesunięcie to ma działać tylko na litery, zaś wszelkie inne znaki (cyfry, spacje, znaki interpunkcyjne itp.) pozostawiać niezmienione. Ma też działać równie dobrze na małe i na wielkie litery.

Napisz następnie funkcję `ceasar_code`, która przyjmie dwa argumenty, string `text` i liczbę całkowitą `shift`, a zwróci zakodowany tekst.

Przyda ci się zdefiniowany poniżej string `ALPHABET`.

Sprawdź swoją odpowiedź: Tekst `'I love coding in Python!!'` po zaszyfrowaniu z przesunięciem 3 ma postać `L oryh frglqj lq Sbwkrq!!`. Sprawdź też, czy po odkodowaniu (przesunięcie -3) otrzymasz oryginalny tekst.

Wskazówka do części pierwszej: Aby uzyskać przesunięcie litery, wyznacz najpierw indeks litery `letter` w alfabecie `ALPHABET` za pomocą metody `index`, a następnie powiększ go o wartość `shift`. Użyj arytmetyki modulo długość stringu `ALPHABET` (wynosi ona 26), aby przejść od indeksów "późnych" liter na początek alfabetu; np. indeks litery `'z'` to 25, po dodaniu 3 to 28, ale modulo 26 daje to 2, czyli indeks litery `'c'` - przyda się operator reszty z dzielenia `%`.

Zawrzyj to w instrukcji warunkowej sprawdzającej, czy znak jest literą (metoda stringów `isalpha`).

Co więcej, przesunięcia dokonuj zawsze na literach zmienionych na małe (jako że tylko małe litery mamy w stringu `ALPHABET`), a następnie zwracaj przesuniętą literę albo małą, albo dużą, zależnie od wielkości oryginalnej litery.

Wskazówka do części drugiej: Aby zakodować cały tekst `text`, zakoduj każdą literę z osobna używając zdefiniowanej w poprzednim kroku funkcji `ceasar_shift` i składni "list comprehension". Następnie połącz tak otrzymaną listę znaków w jeden string za pomocą metody stringów `join`.

```python
ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
```

```python
# dłuższe ćwiczenie 10 - rozwiązanie, część pierwsza

```

```python
# dłuższe ćwiczenie 10 - rozwiązanie, część druga

```

 Dłuższe ćwiczenie 11: Kod Morse'a.

Wczytaj załączony do tej lekcji plik `morse_code.txt` z [kodem Morse'a](https://en.wikipedia.org/wiki/Morse_code) i zapisz go w słowniku `morse_code_dict` - wykonaj po prostu poniższą komórkę. Przydadzą się też zdefiniowane niżej zmienne `SHORT_GAP` (trzy spacje) i `MEDIUM_GAP` (siedem spacji), które konwencyjnie rozdzielają odpowiednio litery i słowa zapisane w języku Morse'a.

(a) Napisz funkcję `morse_code_word`, która przyjmie jeden argument, string `word` (zakładamy, składający się z jednego tylko słowa, lecz pisanego dowolnie małymi lub wielkimi literami), a zwróci inny string, jego reprezentację w języku Morse'a. Np. słowo `'SOS'` to `'...   ---   ...'`, gdzie zwróćmy uwagę na odstęp trzech spacji między literami.

(b) Napisz funkcję `morse_code_text`, która przyjmie jeden argument, string `text` (składający się potencjalnie z wielu słów), a zwróci inny string, jego reprezentację w języku Morse'a. Np. tekst `'I love coding in Python!!'` ma postać:
```
'..       .-..   ---   ...-   .       -.-.   ---   -..   ..   -.   --.       ..   -.       .--.   -.--   -   ....   ---   -.   -.-.--   -.-.--'
```
gdzie mamy siedmio-spacjowe odstępy miedzy wyrazami.

(c) Stwórz słownik `morse_code_dict_reverse`, w którym klucze i wartości to odpowiednio wartości i klucze słownika `morse_code_dict`.

(d) Napisz funkcję `morse_decode_word` dekodującą pojedyncze słowo napisane w języku Morse'a. Zakładamy, że słowo po odkodowaniu składa się jedynie z wielkich liter.

(e) Napisz funkcję `morse_decode_text` dekodującą cały tekst zapisany w języku Morse'a.

Wskazówka: Używaj składni "list comprehension" i odpowiedniego słownika do kodowania/dekodowania. Powstałą tak listę łącz metodą `join` z odpowiednim separatorem, `SHORT_GAP` (w funkcji `morse_code_word`), `MEDIUM_GAP` (w funkcji `morse_code_text`), `''` (w funkcji `morse_decode_word`), czy też `' '` (w funkcji `morse_decode_text`).

W funkcji `morse_code_word` pamiętaj ponadto o przekonwertowaniu każdej litery słowa na wielkie litery (metoda `upper`). Kiedy natomiast tworzysz listę do iteracji w powyższych "list comprehension", przyda się metoda `split`, odpowiednio po `' '` (w funkcji `morse_code_text`), `SHORT_GAP` (w funkcji `morse_decode_word`), `MEDIUM_GAP` (w funkcji `morse_decode_text`)

```python
morse_code_dict = {}
with open( 'Files/morse_code.txt' ) as f:
    for line in f:
        key , val = line.split()
        morse_code_dict[ key ] = val
```

```python
SHORT_GAP = '   '
MEDIUM_GAP = '       '
```

```python
# dłuższe ćwiczenie 11a - rozwiązanie

```

```python
# dłuższe ćwiczenie 11b - rozwiązanie

```

```python
# dłuższe ćwiczenie 11c - rozwiązanie

```

```python
# dłuższe ćwiczenie 11d - rozwiązanie

```

```python
# dłuższe ćwiczenie 11e - rozwiązanie

```

 Dłuższe ćwiczenie 12: Masz daną listę liczb całkowitych `lst`, np. `lst = [5, 3, 2, 3, 1, 4, 2]`.

(a) Posortuj ją w taki sposób, aby wszystkie liczby parzyste były przed wszystkimi liczbami nieparzystymi (lecz względny porządek zarówno pośród liczb parzystych, jak i pośród liczb nieparzystych, pozostał taki sam, jak w liście `lst`). Czyli tu dostalibyśmy wynik `[2, 4, 2, 5, 3, 3, 1]`.

(b) Posortuj ją znowu tak, aby wszystkie liczby parzyste były przed wszystkimi liczbami nieparzystymi, lecz teraz aby zarówno liczby parzyste, jak i nieparzyste, były także pośród siebie posortowane. Czyli tu mielibyśmy resultat `[2, 2, 4, 1, 3, 3, 5]`.

Wskazówka: Użyj wbudowanej funkcji `sorted` z kluczem `key`. Musisz napisać w obu przypadkach odpowiednią funkcję `lambda`. W punkcie (a) chcesz przetransformować listę `lst` w celach porównawczych używając reszty z dzielenia przez 2 - reszta zero pójdzie pierwsza, reszta jeden następna. W punkcie (b) chcesz sortować wedle wartości tupli 2-elementowej, gdzie jej pierwszy element to reszta z dzielenia przez 2, a drugi to sam element.

```python
# dłuższe ćwiczenie 12a - rozwiązanie

```

```python
# dłuższe ćwiczenie 12b - rozwiązanie

```

 Dłuższe ćwiczenie 13: Masz daną listę stringów `lst`, np. `lst = ['ccba', 'bca', 'aba', 'aaa', 'bac', 'a']`. Posortuj ją wedle pozycji litery `'a'` w wyrazie, tj. czym wcześniej litera `'a'` występuje w stringu, tym wcześniej powinien być na posortowanej liście (jeśli pierwsze wystąpienie litery `'a'` w różnych stringach jest na tym samym miejscu, to ich względna pozycja nie ulega zmianie). Czyli tu dostalibyśmy wynik `['aba', 'aaa', 'a', 'bac', 'bca', 'ccba']`.

Wskazówka: Kluczem do sortowania powinien być indeks pierwszego wystąpienia litery `'a'`.

```python
# dłuższe ćwiczenie 13 - rozwiązanie

```

 Dłuższe ćwiczenie 14: Napisz funkcję `sum_of_digits`, która obliczy sumę cyfr dodatniej liczby całkowitej `n`, podanej jej jako jedyny argument. Widzieliśmy już, jak rozwiązać to zadanie bardzo elegancko za pomocą składni "list comprehension" i konwersji między typami `int` a `str`. Tutaj jednak rozwiąż to zadanie _rekursywnie_ - niech funkcja `sum_of_digits` wywołuja sama siebie w swoim ciele.

Wskazówka: Zauważ, iż mając liczbę `n`, np. `n = 12345`, operacja reszty z dzielenia przez 10 daje ostatnią jej cyfrę, `12345 % 10` jest równe `5`. Czyli mamy już ostatnią cyfrę. Jak teraz jej się "pozbyć"? Otóż część całkowita z dzielenia przez 10 dokładnie ją "wycina", `12345 // 10` jest równe `1234`.

Krok rekursywny implementuje to rozumowanie: Funkcja `sum_of_digits` powinna zwracać resztę z dzielenia `n` przez 10 (tj. ostatnią cyfrę liczby `n`) plus wartość samej siebie wywołanej na liczbie `n // 10`, a więc pozbawionej tejże ostatniej cyfry.

Warunek brzegowy jest taki, że kiedy wreszcie `n` stanie się równe `0` (po "usunięciu" ostatniej cyfry, jaka pozostała), to niech funkcja `sum_of_digits` zwraca `0`.

```python
# dłuższe ćwiczenie 14 - rozwiązanie

```

 Dłuższe ćwiczenie 15 (\*): Quicksort.

["Quicksort"](https://en.wikipedia.org/wiki/Quicksort) to efektywny i cały czas popularny algorytm sortowania, opracowany przez Tony'ego Hoare'a w 1959 r. W tym ćwiczeniu napisz funkcję `quicksort`, która przyjmuje jako jeden argument listę `lst`, a zwraca listę w wersji posortowanej (od najmniejszego do największego elementu); spróbuj napisać tę funkcję rekursywnie.

Zasada algorytmu "quicksort" jest bardzo prosta:

- Wybierz element listy `lst` o dowolnym indeksie, tzw. "pivot". Np. mamy listę `lst = [5, 3, 2, 3, 1, 4, 2]` i powiedzmy, że wybieramy element o indeksie 3, czyli drugi element `3` - to nasz "pivot".

- Iterując się przez listę `lst`, stwórz trzy listy: (1) listę wszystkich elementów mniejszych od elementu "pivot" (nazwijmy ją `left`), (2) listę wszystkich elementów równych elementowi "pivot" (nazwijmy ją `middle`), (3) listę wszystkich elementów większych od elementu "pivot" (nazwijmy ją `right`). W naszym przykładzie mielibyśmy `left = [2, 1, 2]`, `middle = [3, 3]` i `right = [5, 4]`.

- Teraz zastosuj algorytm "quicksort" rekursywnie do listy `left` i do listy `right` (to nam je rekursywnie posortuje), a na końcu połącz ("concatenate") ze sobą (1) tak posortowaną listę `left`, (2) listę `middle`, (3) tak posortowaną listę `right`.

- Warunkiem brzegowym rekursji jest, iż jeśli lista `lst` jest pusta, to zwróć ją samą.

Uwaga odnośnie pierwszego kroku: Wybór elementu "pivot" jest dowolny, natomiast okazuje się, że "mądry" jego wybór może zdecydowanie przyspieszyć działanie algorytmu, zaś "niemądry" wybór go spowolnić. Istnieją zatem złożone algorytmy jego wyboru. Tu nie będziemy się tym przejmować - postanówmy, że wybór elementu "pivot" dokonujemy w połowie, tj. jego indeks niech będzie połową długości listy `lst` (dokładniej: częścią całkowitą jej dzielenia przez 2).

Wskazówka: Do wyboru elementu "pivot" przyda się operator `//`. Konstrukcji list `left`, `middle`, `right` dokonaj za pomocą składni "list comprehension". Krok rekursywny zwraca złączenie list, więc przyda się operator `+`; połącz posortowaną przez `quicksort` listę `left`, następnie listę `middle`, następnie posortowaną przez `quicksort` listę `right`. Nie zapomnij o warunku brzegowym - zapisz go elegancko, pamiętając, iż Python traktuje pustą listę jak wartość logiczną `False`.

```python
# dłuższe ćwiczenie 15 - rozwiązanie

```

### 4.2. Projekty końcowe

 Dłuższe ćwiczenie 16: Zabawa z kalendarzem.

To będzie nasz pierwszy projekt końcowy! Napiszemy serię funkcji związanych z utworzeniem "kartki z kalendarza".

W biblitece standardowej Pythona mamy [moduł `calendar`](https://docs.python.org/3/library/calendar.html) zawierający wiele przydatnych funkcji związanych z kalendarzem. W szczególności, mamy funkcję `month`, która zwraca string opisujący "kartkę z kalendarza" na dany miesiąc. Wydrukowanie tego stringu za pomocą funkcji `print` wyświetla go w ładnie sformatowanym stylu. Naszym zadaniem będzie napisanie serii funkcji pozwalających odtworzyć tę funkcjonalność.

```python
from calendar import month
```

```python
month( 2020 , 3 )
```

```python
print( month( 2020 , 3 ) )
```

Zdefiniujmy najpierw kilka przydatnych zmiennych:

- lista nazw miesięcy `MONTHS`;

- lista skrótowych (2-literowych) nazw dni tygodnia `WEEKDAYS`, w konwencji takiej, iż zaczynamy od poniedziałku;

- string `WEEKDAYS_HEADER` o postaci dokładnie takiej, jak nagłówek z dniami tygodnia otrzymywany z funkcji `calendar.month`, tj. `'Mo Tu We Th Fr Sa Su'`;

- jego długość `CALENDAR_WIDTH` (równa 20), która jednocześnie definiuje nam szerokość "kartki z kalendarza".

```python
MONTHS = [ 'January' , 'February' , 'March' , 'April' , 'May' , 'June' , 'July' , 'August' , 'September' , 'October' , 'November' , 'December' ]
```

```python
WEEKDAYS = [ 'Mo' , 'Tu' , 'We' , 'Th' , 'Fr' , 'Sa' , 'Su' ]
WEEKDAYS_HEADER = ' '.join( WEEKDAYS )
CALENDAR_WIDTH = len( WEEKDAYS_HEADER )
```

Część (a): Napisz funkcję `display_header` przyjmującą dwa argumenty: string `month` (np. `'March'`) i liczbę całkowitą `year` (np. `2020`), a zwracającą string będący "nagłówkiem kartki z kalendarza", a zatem składający się z dwóch linijek: w pierwszej linijce wyśrodkowany tekst zawierający miesiąc i rok, a w drugiej wspomniany wyżej nagłówek z dniami tygodnia (np. `'     March 2020\nMo Tu We Th Fr Sa Su'`).

Wskazówka do (a): Wyśrodkowanie uzyskaj albo metodą stringów `center`, albo manualnie, poprzez obliczenie liczby spacji przed i po tekście z miesiącem i rokiem; liczbę spacji przed oblicz jako szerokość "kartki" minus długość tekstu, podzielone przez dwa i z tego część całkowita (np. użyj operatora `//`). Pamiętaj, iż nową linię otrzymujemy przez "escape character" `'\n'`.

```python
# dłuższe ćwiczenie 16a - rozwiązanie

```

Część (b): Widzimy, że dni w naszej "kartce z kalendarza" wydrukowane są tak, iż jeśli dzień jest liczbą 1-cyfrową, to i tak zajmuje on dwa znaki, gdzie pierwszy jest spację, np. `' 8'`. Napisz funkcję `double_digit_str` przyjmującą jeden argument, liczbę całkowitą `day` (np. `8`), a zwracający jej reprezentację jako string, przy czym 1-cyfrowe dni są rozszerzone spację j.w.

Wskazówka do (b): Użyj konwersji na string, prostej instrukcji warunkowej sprawdzającej jego długość i operatora `+`.

```python
# dłuższe ćwiczenie 16b - rozwiązanie

```

Część (c): Napisz funkcję `display_week`, która zwróci string zawierający dni pojedynczego tygodnia z "kartki z kalendarza", np.:
```
' 2  3  4  5  6  7  8'
```
to drugi tydzień marca 2020. Niech przyjmuje ona dwa argumenty, liczby całkowite `week_beginning` (pierwszy dzień tego tygodnia, np. `2`) i `n_days` (całkowita liczba dni w miesiącu, np. `31`). Przyjmijmy konwencję taką, iż `week_beginning` może być ujemne, np. pierwszy tydzień marca 2020 miałby wartość tego argumentu `-5`. Natomiast wyświetlić się muszą oczywiście tylko dni tego tygodnia, ale ograniczone od 1 do `n_days`, czyli np. pierwszy tydzień marca 2020 to:
```
'                   1'
```

Wskazówka do (c): Utwórz najpierw listę dni z tygodnia zaczynającego się w `week_beginning`, przekonwertowanych od razu napisaną w punkcie (b) funkcją `double_digit_str`; użyj składni "list comprehension" z warunkiem `if-else` (przypomnij sobie tę szczególną składnię!), który upewni się, że wyświetlasz dni tylko od 1 do `n_days`, a w przeciwnym przypadku podwójną spację `'  '`. Następnie połącz tę listę metodą stringów `join` z separatorem będącym pojedynczą spacją `' '`.

```python
# dłuższe ćwiczenie 16c - rozwiązanie

```

Część (d): W punkcie (c) napisaliśmy funkcję wyświetlającą pojedynczy tydzień z miesiąca na podstawie jego pierwszego dnia, w konwencji takiej, że może to być liczba ujemna, co oznacza, iż tydzień taki zaczyna się w poprzednim miesiącu. W tym punkcie napiszemy funkcję obliczającą wszystkie te "pierwsze dni" tygodnia. Mianowicie, napisz funkcję `week_beginnings` przyjmującą dwa argumenty: `first_weekday` i `n_days`; pierwszy argument to string opisujący dzień tygodnia (w naszej konwencji 2-literowej), którym jest pierwszy tego miesiąca, np. 1 marca 2020 to niedziela, zatem string ten byłby tu `'Su'`; drugi argument to całkowita liczba dni w miesiącu. Niech funkcja ta zwraca listę pierwszych dni każdego tygodnia w tym miesiącu, np. dla marca 2020 byłoby to `[-5, 2, 9, 16, 23, 30]`.

Wskazówka do (d): Użyj funkcji `range` z trzema argumentami. Drugi i trzeci argument tej funkcji są proste: chcesz iść w krokach co 7 do maksymalnie `n_days` włącznie. Natomiast jaki jest pierwszy argument, opisujący pierwszy element listy? Dla przykładu, jeśli `first_weekday` jest równe `'Su'`, czyli dzień na liście `WEEKDAYS` o indeksie 6, to chcesz iść od dnia -5, co jest równe 1 - 6. Uzasadnij tę zależność, a następnie posłuż się metodą list `index`.

```python
# dłuższe ćwiczenie 16d - rozwiązanie

```

Część (e): Napisz funkcję `display_weeks`, która wydrukuje całą "kartkę z kalendarza", jeszcze bez "nagłówka", tj. wszystkie dni tygodnia, wydrukowane tak samo jak we wbudowanej funkcji `calendar.month`. Niech przyjmuje dwa argumenty, te same co funkcja `week_beginnings` z punktu (d), tj. `first_weekday` (dzień tygodnia, którym jest pierwszy dzień danego miesiąca, w formacie 2-literowego skrótu, np. `'Su'`) i `n_days` (całkowita liczba dni w tym miesiącu). Niech zwraca string, złożony z odpowiedniej liczby linijek, gdzie każda linijka to odpowiedni tydzień, np. dla marca 2020:
```
'                   1\n 2  3  4  5  6  7  8\n 9 10 11 12 13 14 15\n16 17 18 19 20 21 22\n23 24 25 26 27 28 29\n30 31               '
```
co po wydrukowaniu funkcją `print` daje:
```
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
```

Wskazówka do (e): W ciele funkcji utwórz najpierw listę pierwszych dni każdego tygodnia za pomocą funkcji `week_beginnings` z punktu (d). Następnie skonstruuj listę - za pomocą składni "list comprehension", iterując po tej liście pierwszych dni tygodnia - której każdym elementem jest string opisujący dany tydzień, otrzymany za pomocą funkcji `display_week` z punktu (c). Tak otrzymaną listę połącz metodą stringów `join`, z separatorem `'\n'`.

```python
# dłuższe ćwiczenie 16e - rozwiązanie

```

Część (f): Dzięki funkcjom napisanym w punktach (a)-(e) możemy już ładnie wydrukować naszą "kartkę z kalendarza". Jednakże funkcje te opierają się na założeniu, iż podajmy im jako argumenty pewne szczegółowe dane, jak np. to, jakim dniem tygodnia zaczyna się dany miesiąc, czy ile w miesiącu jest dni. W kolejnych punktach zautomatyzujemy podawanie tych informacji, jedynie na podstawie danego miesiąca i roku.

Napisz najpierw funkcję `is_leap` z jednym argumentem, liczbą całkowitą `year` (np. `2020`), która zwraca wartość logiczną odpowiadającą na pytanie, czy dany rok jest przestępny. Zasada jest następująca:

- jeśli rok jest podzielny przez 400, to jest przestępny;

- z pozostałych lat, jeśli rok jest podzielny przez 100, to nie jest przestępny;

- z pozostałych lat, jeśli rok jest podzielny przez 4, to jest przestępny;

- wszystkie pozostałe lata nie są przestępne.

Wskazówka do (f): Możesz zapisać to za pomocą kolejnych instrukcji warunkowych (i oczywiście operatora `%`). Ale spróbuj napisać to w jednej linijce, używając operatorów logicznych i odpowiednio umieszczając nawiasy.

```python
# dłuższe ćwiczenie 16f - rozwiązanie

```

Sprawdź swoją odpowiedź w (f): Zaimportuj funkcję wbudowaną `isleap` z modułu `calendar` i sprawdź, czy jej wyniki są równe twoim dla kilku tysięcy lat.

```python
from calendar import isleap
```

```python
all( [ calendar.isleap( year ) == is_leap( year ) for year in range( 3000 ) ] )
```

Część (g): Napisz funkcję `number_of_days`, która przyjmie dwa argumenty: `month` (będący stringiem, jednym z listy `MONTHS`) i `leap` (będący obiektem typu Boolean), a zwróci liczbę dni w tym miesiącu: 28 lub 29 dla lutego, 30 dla kwietnia, czerwca, września, listopada, a 31 dla pozostałych.

```python
# dłuższe ćwiczenie 16g - rozwiązanie

```

Część (h): W tym punkcie napiszemy funkcję `julian_day_number` implementującą tzw. [datę juliańską](https://en.wikipedia.org/wiki/Julian_day). Wbrew swojej polskiej nazwie, nie jest to data, ale liczba całkowita, mianowicie liczba dni, jakie upłynęły od umownej daty 24 listopada 4714 roku p.n.e., licząc według tzw. [proleptycznego kalendarza gregoriańskiego](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar), czyli przedłużając obecny kalendarz gregoriański wstecz w czasie od jego wprowadzenia w 1582 r. Także wbrew swojej nazwie, nie ma ona nic wspólnego z Juliuszem Cezarem i kalendarzem juliańskim, lecz pochodzi ona od Juliusza Cezara Scaligera, ojca francuskiego uczonego Josepha Justusa Scaligera, który ją wprowadził.

Data juliańska używana jest przede wszystkim w obliczeniach astronomicznych. Jest też prostym sposobem na obliczenie liczby dni, jakie upłynęły między dwoma danymi datami - wystarczy po prostu odjąć od siebie odpowiadające tym datom liczby juliańskie. Innym jej zastosowaniem jest odpowiedź na pytanie, jakim dniem tygodnia była dana data - i to właśnie wykorzystamy w punkcie (i) poniżej.

Uwaga: Bardziej precyzyjnie, data juliańska odnosi się nie tylko do _dnia_, ale do dokładnej godziny. W szczególności, dzień liczy się od południa czasu UTC. My jednak nie przejmujmy się tym i obliczmy uproszczoną wersję daty juliańskiej, opisującą jedynie dzień.

Funkcja `julian_day_number` niech przyjmuje trzy argumenty: `day` (liczba całkowita, dany dzień), `month` (string, jak w liście `MONTHS`, opisujący miesiąc), `year` (liczba całkowita, dany rok). Procedura jest następująca: Miesiącom przypisane są ich kolejne liczby porządkowe, przy czym jeśli miesiąc to styczeń lub luty, to należy: (1) od roku `year` odjąć 1, (2) do liczby porządkowej miesiąca dodać 12. Innymi słowy, miesiącom przypisane są liczby: 3 dla marca, 4 dla kwietnia, ..., 12 dla grudnia, 13 dla stycznia, 14 dla lutego, przy czym styczeń i luty traktowane są jako miesiące _poprzedniego_ roku. W ciele funkcji zdefiniujmy zatem zmienne `Y` i `M` opisujące rok i miesiąc w tej konwencji. Np. styczeń 2020 miałby `Y = 2019` i `M = 13`. Na podstawie tych zmiennych, data juliańska dana jest wzorem:
```
Y * 365 + Y // 4 - Y // 100 + Y // 400 + ( 153 * M - 7 ) // 5 + day + 1721029
```

```python
# dłuższe ćwiczenie 16h - rozwiązanie

```

Sprawdź swoją odpowiedź w (h): Data juliańska dnia 24 listopada -4713 (czyli 4714 r. p.n.e.; pamiętajmy, że nie było roku zerowego naszej ery!) to 0, dnia 16 listopada 1858 to 2400000, 8 marca 2020 to 2458917, zaś 31 sierpnia 2132 to 2500000.

```python
for date in [ ( 24 , 'November' , -4713 ) , ( 16 , 'November' , 1858 ) , ( 8 , 'March' , 2020 ) , ( 31 , 'August' , 2132 ) ]:
    print( julian_day_number( *date ) )
```

Część (i): Okazuje się, że reszta z dzielenia przez 7 daty juliańskiej dokładnie opisuje dzień tygodnia tej daty, gdzie 0 to poniedziałek, 1 wtorek itd. Napisz funkcję `weekday`, przyjmującą trzy argumenty, te same, co wyżej, tj. `day`, `month`, `year`, a zwracającą dzień tygodnia tej daty, w naszej konwencji 2-literowych skrótów obecnych w liście `WEEKDAYS`, np. `'Mo'`.

```python
# dłuższe ćwiczenie 16i - rozwiązanie

```

Sprawdź swoją odpowiedź: Daty urodzenia kilku sławnych uczonych wypadają w następujące dni tygodnia:

| Uczony | Data urodzenia | Dzień tygodnia |
| --- | --- | --- |
| <center>Mikołaj Kopernik</center> | <center>19 lutego 1473</center> | <center>środa</center> |
| <center>Galileusz</center> | <center>15 lutego 1564</center> | <center>sobota</center> |
| <center>Isaac Newton</center> | <center>4 stycznia 1643</center> | <center>niedziela</center> |
| <center>Leonhard Euler</center> | <center>15 kwietnia 1707</center> | <center>piątek</center> |
| <center>Carl Friedrich Gauss</center> | <center>30 kwietnia 1777</center> | <center>środa</center> |
| <center>James Clerk Maxwell</center> | <center>13 czerwca 1831</center> | <center>poniedziałek</center> |
| <center>Albert Einstein</center> | <center>14 marca 1879</center> | <center>piątek</center> |
| <center>Kurt Gödel</center> | <center>28 kwietnia 1906</center> | <center>sobota</center> |
| <center>Alan Turing</center> | <center>23 czerwca 1912</center> | <center>niedziela</center> |
| <center>Benoit Mandelbrot</center> | <center>20 listopada 1924</center> | <center>czwartek</center> |

```python
birthdates = {
    'Nicolaus Copernicus' : ( 19 , 'February' , 1473 ) ,
    'Galileo Galilei' : ( 15 , 'February' , 1564 ) ,
    'Isaac Newton' : ( 4 , 'January' , 1643 ) ,
    'Leonhard Euler' : ( 15 , 'April' , 1707 ) ,
    'Carl Friedrich Gauss' : ( 30 , 'April' , 1777 ) ,
    'James Clerk Maxwell' : ( 13 , 'June' , 1831 ) ,
    'Albert Einstein' : ( 14 , 'March' , 1879 ) ,
    'Kurt Gödel' : ( 28 , 'April' , 1906 ) ,
    'Alan Turing' : ( 23 , 'June' , 1912 ) ,
    'Benoit Mandelbrot' : ( 20 , 'November' , 1924 )
}

correct_answers = [ 'We' , 'Sa' , 'Su' , 'Fr' , 'We' , 'Mo' , 'Fr' , 'Sa' , 'Su' , 'Th' ]

for ( name , ( day , month , year ) ) , correct_answer in zip( birthdates.items() , correct_answers ):
    verdict = 'CORRECT' if weekday( day , month , year ) == correct_answer else 'INCORRECT'
    print( f'{name} was born on {day} {month} {year}, which is... {weekday( day , month , year )}, and which is {verdict}.' )
```

Część (j): Napisz finalną funkcję tego zadania, `display_calendar`, która przyjmie dwa argumenty, `month` (string z listy `MONTHS`) i `year` (liczba całkowita), a zwróci string opisujący pełną "kartkę z kalendarza" dla danego miesiąca i roku.

Wskazówka do (j): W ciele funkcji dokonaj następujących obliczeń:

- zdefiniuj zmienną typu Boolean `leap`, która odpowie na pytanie, czy rok `year` jest przestępny - użyj funkcji `is_leap` z punktu (f);

- zdefiniuj zmienną `n_days`, wyznaczającą liczbę dni tego miesiąca - użyj funkcji `number_of_days` z punktu (g);

- zdefiniuj zmienną `first_weekday`, będącą stringiem opisującym dzień tygodnia, jakim jest dzień pierwszy tego miesiąca - użyj funkcji `weekday` z punktu (i);

- wreszcie skomponuj "kartkę z kalendarza", tworząc jej nagłówek - za pomocą funkcji `display_header` z punktu (a) - oraz kolejne dni tygodnia - za pomocą funkcji `display_weeks` z punktu (e).

```python
# dłuższe ćwiczenie 16j - rozwiązanie

```

Sprawdź swoją odpowiedź: "Kartka z kalendarza" dla marca 2020 powinna mieć postać:
```
'     March 2020\nMo Tu We Th Fr Sa Su\n                   1\n 2  3  4  5  6  7  8\n 9 10 11 12 13 14 15\n16 17 18 19 20 21 22\n23 24 25 26 27 28 29\n30 31               '
```
co po wydrukowaniu funkcją `print` wygląda tak:
```
     March 2020
Mo Tu We Th Fr Sa Su
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
```

```python
display_calendar( 'March' , 2020 )
```

```python
print( display_calendar( 'March' , 2020 ) )
```

 Dłuższe ćwiczenie 17: Kółko i krzyżyk.

To będzie nasz drugi projekt końcowy! Napiszemy kompletny program realizujący grę w kółko i krzyżyk (na planszy 3 x 3), łącznie ze sztuczną inteligencją komputera!

Plansza do gry reprezentowana będzie przez listę list - listę o trzech elementach, każdy z nich także będący listą o trzech elementach, które to należą do zestawu stringów: `'X'` (dane pole zajęte przez X), `'O'` (dane pole zajęte przez O), `' '` (dane pole puste). Np. `[['X', ' ', ' '], ['X', ' ', 'O'], [' ', 'O', 'X']]`. Graczy nazwiemy `'Player'` i `'Computer'`.

Przydadzą się następujące importy:

- Funkcja `random.choice` przyjmuje jako argument listę, a zwraca losowo wybrany jej element. Przyda nam się przy wyborze pierwszego gracza, a także jako część strategii gry komputera.

- Funkcja `copy.deepcopy` dokonuje tzw. "kopii głębokiej" listy list. Jest to związane z zagadnieniem wspominanym w Lekcji 5, mutowalności list. Jeśli mamy listę list - a takim właśnie obiektem będzie nasza plansza do gry - to jeżeli utworzymy jej zwykłą kopię (metodą list `copy`), a następnie w tej kopii zmienimy wartość elementu pod-listy, to okazuje się, iż wartość ta zmieni się także w oryginale! Aby tego uniknąć, należy utworzyć "kopię głęboką". Przyda nam się to przy pisaniu algorytmu sztucznej inteligencji - komputer utworzy kopię głęboką aktualnej planszy do gry, a następnie na niej będzie rozważał swój kolejny ruch.

```python
from random import choice
from copy import deepcopy
```

```python
choice( range( 100 ) ) # losowy wybór z listy
```

```python
board_example = [ [ 'X' , ' ' , ' ' ] , [ ' ' , ' ' , ' ' ] , [ ' ' , ' ' , ' ' ] ]

board_example_copy = board_example.copy() # zwykła kopia listy list
board_example_copy[ 1 ][ 2 ] = 'O'

print( board_example_copy )
print( board_example ) # zmienna board_example zmieniła wartość!!
```

```python
board_example = [ [ 'X' , ' ' , ' ' ] , [ ' ' , ' ' , ' ' ] , [ ' ' , ' ' , ' ' ] ]

board_example_copy = deepcopy( board_example ) # kopia głęboka listy list
board_example_copy[ 1 ][ 2 ] = 'O'

print( board_example_copy )
print( board_example ) # zmienna board_example NIE zmieniła teraz wartości
```

Część (a): Napisz funkcję `generate_empty_board`, nieprzyjmującą żadnych argumentów, a zwracającą pustą planszę, tj. listę list, gdzie każdy string to `' '`, czyli `[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]`.

Wskazówka do (a): Napisz zagnieżdżoną "list comprehension", gdzie iterujesz się po `range(3)` (możesz użyć niemego iteratora!).

```python
# dłuższe ćwiczenie 17a - rozwiązanie

```

Część (b): Narysuj planszę. Napisz funkcję `display_board`, która przyjmie jeden argument `board` (będący naszym obiektem planszy, tj. listą list j.w.), nie będzie nic zwracać, a jej efektem ubocznym będzie wydrukowanie w ładny sposób planszy `board`, łącznie ze współrzędnymi pól. Np. dla `board = [['X', ' ', ' '], ['X', ' ', 'O'], [' ', 'O', 'X']]` chcemy otrzymać:
```
  1 2 3
a X| |
b X| |O
c  |O|X
```
Współrzędne rzędów nazwiemy a, b, c, zaś kolumn 1, 2, 3 - przez nie będziemy później m.in. definiować ruch gracza czy komputera.

Wskazówka do (b): Utwórz najpierw "nagłówek" z numerami kolumn.

Następnie utwórz listę stringów, gdzie każdy string to będzie kolejny wiersz naszej planszy, łącznie z literką wiersza. Użyj składni "list comprehension", gdzie iterujesz się po złączonych "na suwak" (funkcją `zip`) kolekcjach: stringu `'abc'` (to da ci kolejne literki wierszy) i listy `board` (gdzie każdy jej kolejny element to 3-elementowa lista). Każdy kolejny string w tej iteracji składa się z kolejnej literki wiersza, dołączonej (`+`) do kolejnej pod-listy listy `board`, złączonej metodą `join` z separatorem `|`.

Na koniec połącz "nagłówek" z powyżej utworzoną listą, złączoną metodą `join` z separatorem `'\n'`.

```python
# dłuższe ćwiczenie 17b - rozwiązanie

```

Część (c): Wybierz literę gracza. Napisz funkcję `input_player_letter`, bez argumentów, a która wczytywać będzie (funkcją `input`) od gracza, którą literką chciałby grać, X czy O. Niech zwraca tuplę 2-elementową, której pierwszym elementem jest literka gracza, a drugim literka komputera. Gdyby gracz wpisał coś innego niż `'X'` czy `'O'`, niech pojawi się odpowiedni komunikat, np. `'This is not a valid letter!'`.

Wskazówka do (c): Ciało funkcji niech będzie złożone z pętli `while True`. Zdefiniuj tam zmienną `player_letter` poprzez funkcję `input` z odpowiednim komunikatem, np. `'Choose X or O:'`.

Jeśli wybrana litera będzie jedną z `'X'` czy `'O'`, to zdefiniuj `computer_letter` jako literę "przeciwną" i przerwij pętlę. W przeciwnym wypadku, wydrukuj komunikat o niewłaściwej literze.

Na koniec zwróć tuplę liter.

```python
# dłuższe ćwiczenie 17c - rozwiązanie

```

Część (d): Napisz funkcję `who_starts`, bez argumentów, która zwraca losowo wybrany string spośród `'Player'`, `'Computer'`. Niech też jako efekt uboczny drukuje odpowiednią informację, np. `'Player goes first!'`.

Wskazówka do (d): Użyj funkcji `choice` z modułu `random`.

```python
# dłuższe ćwiczenie 17d - rozwiązanie

```

Część (e): Funkcje pomocniczne. Napisz dwie następujące przydatne funkcje:

(e-1) Funkcja `move_to_indices` przyjmuje jeden argument `move`, którym będzie string 2-znakowy o postaci np. `'1a'` czy `'2c'` itp., wskazujący na żądane pole na planszy - w powyżej zdefiniowanych współrzędnych. Niech zwraca ona tuplę 2-elementową indeksów 0, 1, 2 odpowiadających temu polu. Czyli np. `'1a'` odpowiada indeksom 0, 0, zaś `'2c'` indeksom 2, 1.

(e-2) Funkcja `indices_to_move` jest odwrotna do poprzedniej: przyjmuje dwa argumenty `row` i `col`, będące liczbami 0, 1, 2, a zwraca string 2-znakowy o powyższej postaci. Czyli dla argumentów 2, 1 powinna dawać `'2c'` itd.

Wskazówka do (e-1): String `move` zamień najpierw na tuplę `num, let` stosując konwersję typów funkcją `tuple`. Chcesz zwrócić tuplę indeksów `row, col`. Teraz `col` jest powiązany z `num` w prosty sposób - konwersja na `int` i odjęcie jedynki. Natomiast aby dostać `row` z literki `let` użyj metody `index` na stringu `'abc'`.

Wskazówka do (e-2): Argument `row` zamień na literkę przy użyciu składni nawiasów kwadratowych na stringu `'abc'`. Argument `col` zamień na numerek przez zwiększenie go o jeden i konwersję na string.

```python
# dłuższe ćwiczenie 17e-1 - rozwiązanie

```

```python
# dłuższe ćwiczenie 17e-2 - rozwiązanie

```

Część (f): Lista dostępnych ruchów. Napisz funkcję `available_moves`, która przyjmie jeden argument `board` (będący naszym obiektem planszy - listą list), a zwróci listę wolnych (tj. niezajętych ani przez X, ani przez O) pól, w formacie 2-znakowych stringów j.w. Np. dla planszy `board = [['X', ' ', ' '], ['X', ' ', 'O'], [' ', 'O', 'X']]` powinna zwrócić listę `['2a', '3a', '2b', '1c']`.

Wskazówka do (f): Napisz podwójnie zagnieżdżoną "list comprehension". W pierwszej iteracji idź (iteratorem `row, line`) po `enumerate(board)`, a w drugiej (iteratorem `col, letter`) po `enumerate(line)`, filtrując tylko te pola `letter`, które są równe `' '` (czyli niezajęte). W ten sposób `row` i `col` będą indeksami tychże niezajętych pól. Ale skoro chcemy otrzymać nie indeksy, a 2-znakowe oznaczenia pól, to przekonwertuj `row` i `col` funkcją `indices_to_move` z punktu (e-2).

```python
# dłuższe ćwiczenie 17f - rozwiązanie

```

Część (g): Wpisz ruch gracza. Napisz funkcję `input_player_move`, która przyjmie jeden argument `board` (obiekt planszy). W jej ciele utwórz zmienną `player_move` poprzez wprowadzenie jej wartości (w formacie 2-znakowych współrzędnych) przez gracza (funkcją `input`, z odpowiednim pytaniem, np. `'Where do you want to move:'`). Sprawdź, czy ruch ten jest pośród ruchów dostępnych, a jeśli nie, to wydrukuj odpowiedni komunikat, np. `'Choose another move!'` i kontunuuj proszenie użytkownika o wybór. Na koniec, zwróć `player_move`.

Wskazówka do (g): Utwórz najpierw listę dostępnych ruchów używając funkcji `available_moves` z punktu (f). Następnie pytanie funkcją `input` zawrzyj w pętli `while True`. Jeśli wprowadzony ruch będzie na liście dostępnych ruchów, to pętlę przerwij.

```python
# dłuższe ćwiczenie 17g - rozwiązanie

```

Część (h): Wykonaj ruch. Napisz funkcję `make_move`, przyjmującą trzy argumenty: `board` (aktualną planszę), `letter` (aktualną literę, `'X'` albo `'O'`) oraz `move` (aktualny ruch, w formacie 2-znakowych współrzędnych, np. `'2a'`). Niech funkcja ta nic nie zwraca, ma natomiast efekt uboczny modyfikacji swojego argumentu `board` literą `letter` na pozycji `move`.

Wskazówka do (h): Przekonwertuj najpierw współrzędne `move` na indeksy `row, col` funkcją `move_to_indices` z punktu (e-1). Następnie zmień element listy list `board` o indeksach `row` i `col` na `letter`.

```python
# dłuższe ćwiczenie 17h - rozwiązanie

```

Część (i): Sprawdź zwycięstwo. Zwycięstwo litery `letter` zachodzi wtedy, kiedy jeden z następujących warunków jest prawdziwy: w którymś z rzędów wszystkie trzy pola zajmuje litera `letter`; w którejś kolumnie tak jest; na którejś z dwóch przekątnych tak jest. W tej części zaimplementujemy funkcję sprawdzjącą zwycięstwo litery `letter` na danej planszy `board`. W tym celu napiszmy następujące funkcje pomocnicze:

(i-1) Napisz funkcję `all_equal`, przyjmującą dwa argumenty: listę `lst` i literę `letter`, a zwracającą wartość logiczną odpowiadającą na pytanie, czy wszystkie elementy listy `lst` są równe `letter`.

(i-2) Napisz funkcję `transpose_board`, przyjmującą jeden argument `board` i zwracającą jego tzw. ["transpozycję"](https://en.wikipedia.org/wiki/Transpose), czyli odbicie lustrzane w głównej przekątnej, a więc zmieniające wiersze na kolumny i na odwrót. A więc dla `board = [['X', ' ', ' '], ['X', ' ', 'O'], ['O', ' ', 'X']]` dostalibyśmy `[['X', 'X', 'O'], [' ', ' ', ' '], [' ', 'O', 'X']]` (obejrzyj to sobie przy użyciu funkcji `display_board` z punktu (b)).

(i-3) Napisz funkcję `diagonals`, z jednym argumentem `board`, a zwracającą 2-elementową listę przekątnych planszy `board`. Czyli dla `board = [['X', ' ', ' '], ['X', ' ', 'O'], ['O', ' ', 'X']]` byłaby to lista `[['X', ' ', 'X'], ['O', ' ', ' ']]`.

(i-4) Napisz funkcję `is_winner`, z dwoma argumentami: `board` (aktualna plansza) i `letter` (aktualna litera, `'X'` albo `'O'`). Niech zwraca wartość logiczną odpowiadającą na pytanie, czy litera `letter` zwycięża na tej planszy.

Wskazówka do (i-1): Zmień listę `lst` na zbiór i sprawdź jego równość 1-elementowemu zbiorowi z literą `letter`.

Wskazówka do (i-2): Napisz "list comprehension" po `range(3)` (iterator `i`), gdzie każdym elementem jest inne "list comprehension", po pod-listach `board` (iterator `line`). Chcesz wybrać `line[i]`

Wskazówka do (i-3): Pierwsza diagonala składa się z elementów `board[i][i]`, a druga z `board[2 - i][i]`, gdzie iterator `i` przechodzi przez `range(3)`.

Wskazówka do (i-4): Napisz "list comprehension", gdzie iterujesz się po złączonych (operatorem `+`) listach `board`, `transpose_board(board)` i `diagonals(board)`. W każdym kroku iteracji sprawdź, czy dana pod-lista składa się tylko z liter `letter` (funkcją `all_equal` z punktu (i-1)). Pomocna będzie funkcja wbudowana `any`.

```python
# dłuższe ćwiczenie 17i-1 - rozwiązanie

```

```python
# dłuższe ćwiczenie 17i-2 - rozwiązanie

```

```python
# dłuższe ćwiczenie 17i-3 - rozwiązanie

```

```python
# dłuższe ćwiczenie 17i-4 - rozwiązanie

```

Część (j): Sprawdź, czy plansza jest zapełniona. Napisz funkcję `is_board_full`, z jednym argumentem `board`, która zwróci wartość logiczną odpowiadającą na pytanie, czy plansza jest całkowicie zapełniona, tj. czy nie ma na niej ani jednego pustego pola `' '`.

Wskazówka do (j): Napisz "list comprehension", gdzie iterujesz się po pod-listach `line` planszy `board`. W każdym kroku iteracji obliczaj warunek logiczny, że `' '` jest w `line` (użyj operatora `in`). Na koniec tak otrzymaną listę obłóż funkcją `any`.

```python
# dłuższe ćwiczenie 17j - rozwiązanie

```

Część (k): Zmień turę i literę. Napisz funkcję `change_turn_and_letter`, z dwoma argumentami: `turn` (string `'Player'` lub `'Computer'`) oraz letter (string `'X'` lub `'O'`). Niech funkcja ta zwraca ich "wartości przeciwne", tj. tuplę 2-elementową `new_turn , new_letter`.

Wskazówka do (k): Możesz zrobić to 1-linijkowymi wyrażeniami `if-else`.

```python
# dłuższe ćwiczenie 17k - rozwiązanie

```

Część (l): Wygeneruj ruch komputera. To będzie nasza najdłuższa funkcja - odpowiadającą za "sztuczną inteligencję" komputera w naszej grze. Napisz funkcję `generate_computer_move`, z dwoma argumentami: `board` (aktualna plansza) i `computer_letter` (litera, jaką gra komputer). Niech zwraca ona ruch komputera, jako 2-znakowe współrzędne.

W ciele funkcji najpierw utwórz zmienną `player_letter` jako literę "przeciwną" do `computer_letter`, a także listę dostępnych ruchów na planszy `board`.

Algorytm, jaki tu zapiszemy, składa się z następujących pięciu kroków:

Krok 1: Sprawdź, czy komputer może wygrać tym ruchem. Przeanalizuj wszystkie możliwe warianty najbliższego ruchu komputera (spośród dostępnych ruchów) i jeśli któryś doprowadza do zwycięstwa, to zwróć go.

Krok 2: Sprawdź, czy gdyby to gracz dokonywał następnego ruchu, to mógłby nim wygrać. Jeśli tak, zablokuj ten ruch poprzez wykonanie go samemu - zwróć ten ruch.

Krok 3: Zwróć jako ruch jeden (losowo wybrany) z dostępnych rogów planszy (pola `'1a'`, `'3a'`, `'1c'`, `'3c'`).

Krok 4: Zwróć jako ruch środek planszy (pole `'2b'`).

Krok 5: Zwróć jako ruch jeden (losowo wybrany) z dostępnych boków planszy (pola `'1b'`, `'2a'`, `'2c'`, `'3b'`).

Przypomnij sobie, że napotkany `return` kończy wykonywanie ciała funkcji, a zatem warunki te sprawdzane są w wypisanej kolejności i jeśli tylko któryś zajdzie, to zwracana jest odpowiednia wartość, a wykonywanie ciała funkcji natychmiast się przerywa.

Wskazówka do (l): Krok 1 i 2 piszemy analogicznie:

- Przeiteruj się (iteratorem `move`) przez listę dostępnych ruchów (otrzymaną z funkcji `available_moves` z punktu (f)).

- W każdym kroku utwórz zmienną `virtual_board` będącą "kopią głęboką" planszy `board`.

- Na tej wirtualnej planszy dokonaj ruchu `move` funkcją `make_move` z punktu (h). Ruch jest literą komputera (w Kroku 1) albo gracza (w Kroku 2).

- Napisz instrukcję warunkową sprawdzającą, czy dana litera jest zwycięzcą po tym ruchu - użyj funkcji `is_winner` z punktu (i-4). Jeśli tak, zwróć ruch `move`.

Aby napisać Krok 3 i 5, stwórz najpierw listę dostępnych rogów albo boków, np. poprzez odpowiednie "list comprehension". Jeśli jest niepusta, zwróć losowy jej element (funkcją `choice` z modułu `random`).

W Kroku 4 sprawdź, czy środek planszy jest dostępny, a jeśli tak, to go zwróć.

```python
# dłuższe ćwiczenie 17l - rozwiązanie

```

Część (m): Właściwa gra. Napisz program "kółko i krzyżyk":

- Zdefiniuj zmienną `bo` jako pustą planszę - użyj funkcji `generate_empty_board` z punktu (a).

- Wydrukuj planszę `bo` - użyj funkcji `display_board` z punktu (b).

- Zdefiniuj litery gracza i komputera, `pl, cl` - poprzez funkcję `input_player_letter` z punktu (c).

- Zdefinuj turę `tn` (`'Player'` albo `'Computer'`)  - funkcją `who_starts` z punktu (d). Niech zmienna `let` opisuje literę aktualnego gracza, czyli `pl` jeśli `tn` jest równe `'Player'` i `cl` jeśli `tn` jest równe `'Computer'`.

Główna część programu poniżej zawarta będzie w pętli `while True`:

- Jeśli `tn` jest równe `'Player'`, zdefinuj ruch `m` funkcją `input_player_move` z punktu (g). Jeśli `tn` jest równe `'Computer'`, to `m` niech będzie ruchem komputera, wygenerowanym funkcją `generate_computer_move` z punktu (l). W tym drugim przypadku wydrukuj też odpowiednią wiadomość, np. `'Computer moved to ...'`.

- Wykonaj na planszy `bo` literą `let` ruch `m` - użyj funkcji `make_move` z punktu (h).

- Wydrukuj aktualną planszę `bo` - funkcją `display_board` z punktu (b).

- Sprawdź warunki: Jeśli litera `let` wygrywa - użyj funkcji `is_winner` z punktu (i-4) - to wypisz odpowiedni komentarz (np. `'Player, playing X, is the winner!!!'`) i przerwij pętlę. Jeśli nie, to sprawdź warunek, czy plansza jest całkowicie zapełniona - funkcją `is_board_full` z punktu (j) - i jeśli tak, to wydrukuj odpowiedni komentarz (np. `'The board is full - the game is a tie!'`) i przerwij pętlę. Wreszcie jeśli żaden z tych wariantów nie jest spełniony, zmień turę i literę - funkcją `change_turn_and_letter` z punktu (k).

```python
# dłuższe ćwiczenie 17m - rozwiązanie

```

### 4.3. Zamiast zakończenia

 Gratulacje!!!

Dotarliśmy do końca kursu! Rozwiązaliśmy masę zadań! Ten kurs, choć "podstawowy", wcale taki podstawowy nie był - nauczyliśmy się nie tylko fundamentów składni, ale dotarliśmy do naprawdę zaawansowanych tematów - kochasz i nienawidzisz jednocześnie składnię "list comprehension", nie straszne ci indeksy kolekcji, wiesz co to rekursja, umiesz napisać algorytm sortujący i grę, z łatwością żonglujesz stringami. Poćwiczyliśmy na tuzinach zadań używane na co dzień przez każdego programistę Pythona rozwiązania, jak "list comprehension", słowniki, metody stringów, funkcje lambda. Dowiedzieliśmy się co nieco, jak pisać kod, który określony może być jako "elegancki" - "Pythonic". Zaznajomiliśmy się z myśleniem algorytmicznym. No i dowiedzieliśmy się, jakie supersamochody produkuje LEGO, kto był pradziadkiem Bilbo Bagginsa, jaki jest najtrudniejszy prosty problem w matematyce i że Einstein urodził się w piątek 🙂.

Co dalej?

Pamiętajmy o dwóch sentencjach łacińskich, które przyświecały temu kursowi: "quidquid discis, tibi discis", czyli "czegokolwiek się uczysz, uczysz się dla siebie", a także "repetitio est mater studiorum", czyli "powtarzanie jest matką wiedzy". Nauczyliśmy się tu naprawdę dużo. Aby tego nie zapomnieć, należy te rzeczy ćwiczyć - czy to na zadaniach, czy na praktycznych projektach. Droga do zostania programistą Pythona stoi otworem - z takim fundamentem masz bardzo mocny start.

#### Zadania

Jeśli nie masz jeszcze dość **zadań** 🙂, ćwicz je na wielu dostępnych stronach, np.:

- [Project Euler](https://projecteuler.net/),

- [LeetCode](https://leetcode.com/),

- [Programming Praxis](https://programmingpraxis.com/),

- [Code Wars](https://www.codewars.com/),

- wielu innych stronach i repozytoriach, jak [tutaj](http://pythonpracticeprojects.com/), [tutaj](http://puzzles.bostonpython.com/), [tutaj](https://github.com/blakeembrey/code-problems), czy [tutaj](https://github.com/donnemartin/interactive-coding-challenges).

Wyjątkowym zbiorem wyzwań programistycznych jest ["Build your own X"](https://github.com/danistefanovic/build-your-own-x), gdzie możesz - kodując w różnych językach - samodzielnie napisać program do rozszerzonej rzeczywistości, blockchain, bazę danych, grę, czy system operacyjny! A nawet napisać swój własny język programowania! W ten sposób możesz zapoznać się z najważniejszymi zasadami tych technologii poprzez samodzielne ich kodowanie, zgodnie z dewizą Richarda Feynmana, zostawioną na jego ostatniej tablicy kredowej, "What I cannot create, I do not understand".

Prawdziwą kopalnią trudniejszych problemów jest [repozytorium](https://github.com/norvig/pytudes) Petera Norviga, dyrektora badań w Google. Wiele z jego problemów pochodzi od [The Riddler](https://fivethirtyeight.com/tag/the-riddler/).

Lista ta mogłaby się długo ciągnąć. Część ćwiczeń może wydawać się czasem niepraktyczna ("znowu reszta z dzielenia, kiedy ja chcę robić strony internetowe!?"), ale nie daj się zwieść - rozwiązywanie takich zadań nie tylko kształtuje umysł, ogólne myślenie logiczne i zdolność rozwiązywania problemów, tak u dzieci, jak i dorosłych, ale także przygotowuje do bardzo prawdziwych wyzwań stojących przed programistą. To też filozofia za japońską koncepcją "kata", gdzie przez wielokrotne powtarzanie czynności nabywa się w niej mistrzostwo (zob. np. [tutaj](http://codekata.com/)). Wreszcie, proces rekrutacyjny w wielu firmach technologicznych polega na rozwiązywaniu dokładnie takich ćwiczeń; świetnym przykładem jest tajny proces rekrutacyjny Google, tzw. ["Google foo.bar Challenge"](https://www.freecodecamp.org/news/the-foobar-challenge-googles-hidden-test-for-developers-ed8027c1184/).

#### Tutoriale

Jest mnóstwo wszelkich **tutoriali** Pythona - gość na nich często w poszukiwaniu inspiracji i eleganckich rozwiązań! Wymieńmy choćby:

- [Real Python](https://realpython.com/),

- [W3 Schools](https://www.w3schools.com/python/),

- [ten kurs](https://www.python-course.eu/),

- [Geeks for Geeks](https://www.geeksforgeeks.org/) itd.

Absolutnie kluczowym portalem jest [Stack Overflow](https://stackoverflow.com/) ("stack" to stos - pamiętasz go z Lekcji 5? a problem "stack overflow" może być sposodowany np. przez bardzo głęboką rekursję), forum pytań i odpowiedzi na wszelkie tematy związane z programowaniem. Wiele czasu każdego programisty jest poświęcone na szukaniu tam odpowiedzi!

Rozważ subskrypcję [Medium](https://medium.com/), gdzie znajdziesz niekończący się strumień czasem lepszych, czasem gorszych, a czasem wyśmienitych artykułów na różne tematy, m.in. te związane z Pythonem.

#### Specjalizacja

Zastanów się, jaka część programowania interesuje cię najbardziej i spróbuj zacząć szkolić się w tym **wybranym kierunku**. Na tym kursie poznaliśmy zręby tzw. biblioteki standardowej Pythona, ale prócz niej istnieją niezliczone biblioteki i rozwiązania specjalistyczne, przeznaczone do konkretnych celów.

- Interesuje cię praca z danymi? Fundamentalna będzie `pandas`.

- Chcesz zacząć przygodę z uczeniem maszynowym i sztuczną inteligencją? Prócz `pandas` konieczne będą `numpy`, `scikit-learn` i `TensorFlow`. Przeczytaj też [najlepsze wprowadzenie](https://www.amazon.com/Hands-Machine-Learning-Scikit-Learn-TensorFlow/dp/1491962291) do tematu.

- Chcesz w końcu pisać te strony internetowe!? Poświęć się uczeniu `Django` i `Flask`.

- Chcesz tworzyć wizualizacje i dashboardy? Zapoznaj się z `matplotlib`, `seaborn`, `Bokeh` i `Dash`.

Interesuje cię tworzenie GUIs (Graphical User Interfaces), programownie Raspberry Pi, analiza danych geo-przestrzennych, bioinformatyka, kryptografia, etyczne hakerstwo, pisanie gier, obliczenia naukowe, automatyzacja codziennych zadań na komputerze... wszystko to - i dużo więcej - możesz zrobić w Pythonie. I to... _elegancko_ 😉.

```python

```