Rekurzia a funkcionálne programovanie

Funkcia v Pythone

  • blok kódu, ktorý vykonáva určitú úlohu a môže byť volaný z iných častí programu.
  • má argumenty a má návratové hodnoty
  • funkcia môže byť definovaná pomocou kľúčového slova def alebo lambda.
  • funkcia je objekt, ktorý je možné uložiť do premennej, posielať ako argument alebo vracať z inej funkcie.
         +---------+
vstup -> | funkcia | -> výstup
         +---------+

📚 Definovanie funkcií

Funkcia môže mať predvolené hodnoty argumentov

def ahoj(name="Daniel"):
    return f"Ahoj, {name}!"

pozdrav = ahoj
print(pozdrav())  # "Ahoj, Daniel!"
print(pozdrav("Svet"))  # "Ahoj, Svet!"

Funkcia môže obsahovať typové anotácie a dokumentačné reťazce

def add(a: int, b: int) -> int:
    """Vráti súčet dvoch čísel."""
    return a + b

📚 Dokumentačné reťazce a typové anotácie · 📖 DIP3: Documentation Strings

Testy v dokumentácii funkcie

príklady použitia, ktoré sú zároveň automaticky testovateľné pomocou modulu doctest.

def add(a: int, b: int) -> int:
    """Vráti súčet dvoch čísel.

    >>> add(2, 3)
    5
    >>> add(-1, 1)
    0
    """
    return a + b

📚 Modul doctest · 📖 DIP3: Unit testing

Funkcie sú objekty

  • je možné uložiť ich do premennej
  • je možné ich posielať ako argument
  • funkcia môže byť návratovou hodnotou inej funkcie

📚 Používateľom definované funkcie · 📖 DIP3: Everything Is An Object

Funkciu vieme uložiť do premennej

def ahoj(meno="svet"):
    print(f"Ahoj {meno}")

pozdrav = ahoj
pozdrav()

Triedenie pomocou "kľúčovej" funkcie

Funkcia vráti hodnotu, podľa ktorej sa triedi:

# Triedenie podľa dĺžky slova
words = ["apple", "banana", "cherry", "date"]
def length(word):
    return len(word)

sorted_words = sorted(words, key=length)

Ako by sme triedili podľa počtu samohlások?

Triedenie n-tíc pomocou kľúčovej funkcie

Máme zoznam n-tíc, ktoré obsahujú meno, známku a vek študenta. Chceme ich zoradiť podľa mena a veku.

student_tuples = [
    ('john', 'A', 15),
    ('jane', 'B', 12),
    ('dave', 'B', 10),
]

# Funkcia vyberie a vráti postupnosť hodnôt, podľa ktorých sa má triediť
def name_and_age(student):
    return student[0], student[2]

sorted_students = sorted(student_tuples, key=name_and_age)

📚 Techniky triedenia · 📖 DIP3: sorted() a kľúčová funkcia

Lambda funkcie

Funkciu vieme definovať aj priamo na mieste:

data = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35},
]

# sorted je generátor
sorted_data = sorted(data, key=lambda x: x["age"])
# Výraz za dvojbodkou vráti hodnotu, podľa ktorej sa triedi.

📚 Lambda

Generátorová funkcia je funkcia, ktorú je možné prechádzať ako postupnosť

  • výsledok nie je vyhodnotený naraz, ale postupne
  • produkuje hodnoty pomocou kľúčového slova yield
  • pri ďalšom prechode pokračuje tam, kde skončila
def generate_numbers(n):
    for i in range(n):
        yield i * 2

for number in generate_numbers(10):
    print(number)

📚 Generátory · 📖 DIP3: Closures & Generators

Funkcia je rekurzívna, ak volá samu seba

  • dokáže nahradiť cyklus, a cyklus je možné nahradiť rekurziou.
  • rozdelí problém na menšie podproblémy rovnakého typu.
  • každé ďalšie volanie funkcie vytvorí novú kópiu lokálnych premenných a argumentov.

Rekurzívny súčet zoznamu

def sum_list(items):
    # Ukončovacia podmienka identifikuje triviálny prípad
    # V tomto prípade je to prázdny zoznam, ktorý má súčet 0
    if not items:
        return 0

    # Vyberieme posledný prvok bez úpravy vstupného zoznamu
    last_item = items[-1]
    # výsledok je súčet posledného prvku a súčtu zvyšku zoznamu
    # (Všimnite si: items[:-1] kopíruje zoznam — efektívnejšie je pracovať s indexom.)
    return last_item + sum_list(items[:-1])  # Rekurzívne volanie funkcie

print(sum_list([1, 2, 3, 4])) # 10

Dve časti rekurzie

Každá rekurzívna funkcia potrebuje:

  • ukončovacia podmienka – prípad, ktorý už nevolá ďalšiu rekurziu,
  • rekurzívny krok – volanie tej istej funkcie s menším alebo jednoduchším vstupom.
def factorial(n):
    if n == 0:          # ukončovacia podmienka
        return 1
    return n * factorial(n - 1)  # rekurzívny krok

Priebeh výpočtu

Každé volanie funkcie sa ukladá do pamäte, až kým sa nedosiahne ukončovacia podmienka. Potom sa výsledky postupne vracajú späť.

factorial(4)
= 4 * factorial(3)
= 4 * 3 * factorial(2)
= 4 * 3 * 2 * factorial(1)
= 4 * 3 * 2 * 1 * factorial(0)
= 4 * 3 * 2 * 1 * 1
= 24

Každá rekurzia sa dá zapísať aj pomocou cyklu

Ak je rekurzia príliš hlboká, môže spôsobiť vyčerpanie zásobníka (stack overflow). V takom prípade je vhodné použiť cyklus.

def factorial(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

range je generátor postupností

Čo je funkcionálne programovanie?

Funkcionálne programovanie (FP) je štýl programovania, kde:

  • funkcie sú hlavné stavebné bloky
  • vyhýbame sa meneniu dát (nemeniteľnosť); súčasne sa snažíme nemať vedľajšie efekty (napr. meniť globálne premenné)
  • používame rekurziu a funkcie vyššieho rádu (funkcie, ktoré berú iné funkcie ako argumenty alebo vracajú funkcie).

📚 Functional Programming HOWTO · 📖 DIP3: Closures & Generators

Čisté funkcie

  • vždy vráti rovnaký výsledok pre rovnaký vstup
  • nemá vedľajšie efekty
         +---------+
vstup -> | funkcia | -> výstup
         +---------+

Výhody:

  • menej chýb spôsobených zmenou stavu
  • jednoduchšie testovanie a ladenie
  • ľahšia paralelizácia a distribuované spracovanie

Funkcie s vedľajšími efektami

Menia globálne premenné, čítajú alebo zapisujú do súborov, terminálu, siete, databázy...

            stav
            ^   |
            |   v
         +---------+
vstup -> | funkcia | -> výstup
         +---------+
              |
              v
          vedľajší efekt 

Vonkajší stav sa správa ako skrytý argument a ako skrytá návratová hodnota funkcie

dva zavolania takej funkcie môžu mať dva rôzne výsledky

total = 0

def add(a):
    global total
    print("Ahoj Svet") # zmení stav terminálu
    total += a         # zmení stav pamäte

Testovanie čistých funkcií

  • nepotrebujú globálny stav,
  • nemenia okolie programu,
  • rovnaký vstup vždy dá rovnaký výstup.
def add(a, b):
    return a + b

assert add(2, 3) == 5

📖 DIP3: Unit testing

Funkcia vyššieho rádu

  • má inú funkciu ako argument alebo návratovú hodnotu
def apply_function(f, x):
    return f(x)

print(apply_function(lambda x: x * 2, 5))

📚 Functional Programming HOWTO · 📖 DIP3: A List Of Functions

Funkcie vyššieho rádu pre spracovanie zoznamov

  • map()
  • filter()
  • reduce()
+-----------------+     +---------+   +-----------------+
| vstupný zoznam  |  →  | funkcia | → | výstupný zoznam |
+-----------------+     +---------+   +-----------------+

map()

Zoznam vstupov vytvorí zoznam výstupov transformáciou podľa zadanej funkcie.

numbers = [1, 2, 3]
result = list(map(lambda x: x * 2, numbers))

📚 map() · 📖 DIP3: List Comprehensions

🔵 filter()

Podľa zadanej funkcie vyberie zoznam vstupov, ktoré spĺňajú podmienku.

numbers = [1, 2, 3, 4]
result = list(filter(lambda x: x % 2 == 0, numbers))

📚 filter() · 📖 DIP3: List Comprehensions

🔴 `reduce()``

– redukuje zoznam na jednu hodnotu

+---------+    +---------+   +--------+
| zoznam  | →  | funkcia | → |hodnota |
+---------+    +---------+   +--------+

Funkcia má dva operandy. Prvý operand je akumulátor, druhý je aktuálny prvok zoznamu. Výsledok sa stáva novým akumulátorom.

akumulátor = f(akumulátor, prvok)

📚 functools.reduce()

Príklad na reduce()

Vypočíta súčet zoznamu.

from functools import reduce

numbers = [1, 2, 3, 4]
result = reduce(lambda x, y: x + y, numbers)

📚 functools.reduce()

Generovanie zoznamu - list comprehension

Python má elegantnejšiu alternatívu k map a filter:

numbers = [1, 2, 3, 4]

result = [x * 2 for x in numbers if x % 2 == 0]

📚 List comprehensions · 📖 DIP3: Comprehensions

Uzáver - Closure

Uzáver zostaví funkciu na požiadanie podľa zadaných parametrov.

def multiplier(n):
    def multiply(x):
        return x * n
    return multiply

double = multiplier(2)

print(double(5))

📚 Definovanie funkcií a uzávery · 📖 DIP3: Closures & Generators

Dekorátory sú funkcie vyšieho rádu

Dekorátor je funkcia (uzáver), ktorá modifikuje správanie inej funkcie:

  • berie funkciu ako argument,
  • vracia novú funkciu (uzáver) s pridanou logikou.

📚 Dekorátory

Príklad: zapamätanie výsledkov funkcie (lru_cache)

Každé ďalšie volanie funkcie s rovnakými argumentmi vráti uložený výsledok, namiesto opätovného výpočtu.

Funkcia by mala byť čistá (bez vedľajších efektov), aby sa dala bezpečne cache-ovať.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))     # 12586269025  — okamžite (niekoľko ms)
                       # bez @lru_cache by trvalo minúty

📚 functools.lru_cache

Vlastný dekorátor

meranie času vykonania funkcie

import time

def meraj_cas(f):
    def obalka(*args, **kwargs):
        start = time.perf_counter()
        result = f(*args, **kwargs)
        end = time.perf_counter()
        print(f"{f.__name__}: {end - start:.4f}s")
        return result
    return obalka

@meraj_cas
def pomaly_vypocet():
    total = sum(i ** 2 for i in range(10_000_000))
    return total

pomaly_vypocet()
# pomaly_vypocet: 2.1342s

Dekorátor s parametrami

Parametre cez dvojitý uzáver:

def opakovane(n):
    def decorator(f):
        def obalka(*args, **kwargs):
            for _ in range(n):
                result = f(*args, **kwargs)
            return result  # vráti iba výsledok posledného volania
        return obalka
    return decorator

@opakovane(3)
def ahoj():
    print("Ahoj")

ahoj()
# Ahoj
# Ahoj
# Ahoj

📚 Dekorátory

Operátory * a **

  • * rozbaľuje zoznam alebo n-ticu na jednotlivé argumenty funkcie.
  • ** rozbaľuje slovník na jednotlivé argumenty funkcie typu kľúč hodnota.
def sum_all(*args, **kwargs):
    # povinné argumenty sú v zozname args
    print("args:", args)
    # nepovinné argumenty s predvolenou hodnotou sú v slovníku kwargs
    print("kwargs:", kwargs)

Príklad na dekorátor, ktorý vypíše všetky argumenty funkcie:

def vypis_argumenty(f):
    def obalka(*args, **kwargs):
        print(f"Volanie funkcie {f.__name__} s argumentami:")
        print("args:", args)
        print("kwargs:", kwargs)
        return f(*args, **kwargs)
    return obalka

@vypis_argumenty
def add(a, b=0):
    return a + b
add(1, b=2)

Praktický príklad: Dátový pipeline

Problém: zo zoznamu objednávok odstrániť objednávky s nulovou sumou, vypočítať DPH z každej platnej objednávky a získať súhrnné hodnoty.

Pri peniazoch nepoužívame float, pretože desatinné čísla sa v počítači nedajú vždy reprezentovať presne. Použijeme Decimal a sadzby zapíšeme ako reťazce.

from decimal import Decimal

orders = [
    {"id": 1, "amount": Decimal("100.00"), "vat_rate": Decimal("0.20")},
    {"id": 2, "amount": Decimal("0.00"), "vat_rate": Decimal("0.20")},  # vyraď
    {"id": 3, "amount": Decimal("250.00"), "vat_rate": Decimal("0.10")},
    {"id": 4, "amount": Decimal("80.00"), "vat_rate": Decimal("0.20")},
]

Očakávaný výsledok pre platné objednávky:

suma bez DPH: 430.00
DPH:          61.00
suma s DPH:   491.00

Riešenie imperatívnym štýlom

valid_orders = []
total_net = Decimal("0")
total_vat = Decimal("0")

for order in orders:
    if order["amount"] > 0:
        vat = order["amount"] * order["vat_rate"]
        valid_orders.append({**order, "vat": vat})
        total_net += order["amount"]
        total_vat += vat

total_gross = total_net + total_vat

Riešenie pomocou list comprehension

valid_orders = [o for o in orders if o["amount"] > 0]
with_vat = [
    {**o, "vat": o["amount"] * o["vat_rate"]}
    for o in valid_orders
]
total_net = sum((o["amount"] for o in with_vat), Decimal("0"))
total_vat = sum((o["vat"] for o in with_vat), Decimal("0"))
total_gross = total_net + total_vat

Riešenie funkcionálnym štýlom

from functools import reduce

def is_valid(order):
    return order["amount"] > 0

def add_vat(order):
    vat = order["amount"] * order["vat_rate"]
    return {**order, "vat": vat}

# 1. Odfiltrujeme nulové objednávky.
valid_orders = list(filter(is_valid, orders))

# 2. Vypočítame DPH bez zmeny pôvodných slovníkov.
with_vat = list(map(add_vat, valid_orders))

# 3. Zredukujeme objednávky na súhrnné hodnoty.
summary = reduce(
    lambda result, order: {
        "net": result["net"] + order["amount"],
        "vat": result["vat"] + order["vat"],
    },
    with_vat,
    {"net": Decimal("0"), "vat": Decimal("0")},
)

total_net = summary["net"]
total_vat = summary["vat"]
total_gross = total_net + total_vat

📚 Functional Programming HOWTO · 📖 DIP3: Comprehensions

Paralelizácia má zmysel pri veľkých dátach

from multiprocessing import Pool

def process_order(order):
    if not is_valid(order):
        return None
    return add_vat(order)

if __name__ == "__main__":
    with Pool() as pool:
        results = pool.map(process_order, orders)

    results = [order for order in results if order is not None]
    total_net = sum(
        (order["amount"] for order in results),
        Decimal("0"),
    )
    total_vat = sum(
        (order["vat"] for order in results),
        Decimal("0"),
    )
    total_gross = total_net + total_vat

📚 multiprocessing.Pool

Map a reduce v HPC počítačoch

Hadoop a Spark sú distribuované výpočtové systémy, ktoré využívajú map a reduce na spracovanie veľkých dát.

Príklad: počítanie počtu IP adries z logov z webového servera

MAP: rozdelíme logy na menšie časti a počítame IP adresy pre pre každú časť osobitne paralelne na viacerých uzloch.

REDUCE: zjednotíme čiastkové výsledky z jednotlivých uzlov do jedného výsledku.

+----------+     +-----+   +--------------------+
| časť dát |  →  | map | → | čiastkové výsledky |
+----------+     +-----+   +--------------------+
                              |
                              v
                           +--------+     +----------+
                           | reduce | ->  | výsledok |
                           +--------+     +----------+
                              ^
                              |
+----------+     +-----+   +--------------------+
| časť dát |  →  | map | → | čiastkové výsledky |
+----------+     +-----+   +--------------------+

Zhrnutie

  • funkcionálne programovanie je založené na čistých funkciách, ktoré nemenia stav programu a nemajú vedľajšie efekty. Preto sú vhodné na paralelizáciu a distribuované spracovanie a ľahko sa testujú.
  • funkcie sú objekty, ktoré je možné posielať ako argumenty a vracať z iných funkcií.
  • lambda funkcie sú anonymné funkcie, ktoré sa definujú priamo na mieste.
  • dekorátory sú funkcie vyššieho rádu, ktoré modifikujú správanie iných funkcií.

Teaser: Objektovo orientované programovanie sa často zameriava na mutáciu stavu objektov.

Reload?