5. Podprogramy

Funkcie

Doteraz sme pracovali so štandardnými funkciami, napr.

  • vstup a výstup input() a print()

  • aritmetické funkcie abs() a round()

  • generovanie postupnosti čísel pre for-cyklus range()

Všetky tieto funkcie niečo vykonali (vypísali, prečítali, vypočítali, …) a niektoré z nich vrátili nejakú hodnotu, ktorú sme mohli ďalej spracovať. Tiež sme videli, že niektoré majú rôzny počet parametrov, prípadne sú niekedy volané bez parametrov.

Okrem toho sme pracovali aj s funkciami, ktoré boli definované v iných moduloch:

  • keď napíšeme import random, môžeme pracovať napr. s funkciami random.randint() a random.randrange()

  • keď napíšeme import math, môžeme pracovať napr. s funkciami math.sin() a math.cos()

  • keď napíšeme import tkinter, môžeme pracovať napr. s funkciami tkinter.Canvas() a tkinter.mainloop()

Všetky tieto a tisícky ďalších v Pythone naprogramovali programátori pred nejakým časom, aby nám neskôr zjednodušili samotné programovanie. Vytváranie vlastných funkcií pritom vôbec nie je komplikované a teraz sa to naučíme aj my.

Funkcie

Funkcia je pomenovaný blok príkazov (niekedy sa tomu hovorí aj podprogram). Popisujeme (definujeme) ju špeciálnou konštrukciou:

def meno_funkcie():      # zapamataj si blok prikazov ako novy prikaz
    prikaz
    prikaz
    ...

Keď zapíšeme definíciu funkcie, zatiaľ sa z bloku príkazov (hovoríme tomu telo funkcie) nič nevykoná. Táto definícia sa „len“ zapamätá a jej referencia sa priradí k zadanému menu - vlastne sa do premennej meno_funkcie priradí referencia na telo funkcie. Je to podobné tomu, ako sa priraďovacím príkazom do premennej priradí hodnota z pravej strany príkazu.

Ako prvý príklad zapíšme takúto definíciu funkcie:

def vypis():
    print('**********')
    print('**********')

Zadefinovali sme funkciu s menom vypis, pričom telo funkcie obsahuje dva príkazy na výpis riadkov s hviezdičkami. Celý blok príkazov je odsunutý o 4 medzery rovnako ako sme odsúvali príkazy v cykloch a aj v podmienených príkazoch. Definícia tela funkcie končí vtedy, keď sa objaví riadok, ktorý už nie je odsunutý. Touto definíciou sa ešte žiadne príkazy z tela funkcie nevykonávajú. Na to potrebujeme túto funkciu zavolať.

Volanie funkcie

Volanie funkcie je taký zápis, ktorým sa začnú vykonávať príkazy z definície funkcie. Stačí zapísať meno funkcie so zátvorkami a funkcie sa spustí:

meno_funkcie()

Samozrejme, že funkciu môžeme zavolať až vtedy, keď už Python pozná jej definíciu.

Zavolajme funkciu vypis v príkazovom režime:

>>> vypis()
**********
**********
>>>

Vidíme, že sa vykonali oba príkazy z tela funkcie a potom Python ďalej čaká na ďalšie príkazy. Zapíšme volanie funkcie aj s jej definíciou priamo do skriptu (teda v programovom režime):

def vypis():
    print('**********')
    print('**********')

print('hello')
vypis()
print('* Python *')
vypis()

Skôr, ako to spustíme, si uvedomme, čo sa udeje pri spustení:

  • zapamätá sa definícia funkcie v premennej vypis

  • vypíše sa slovo 'hello'

  • zavolá sa funkcia vypis()

  • vypíše riadok s textom '* Python *'

  • znovu sa zavolá funkcia vypis()

A teraz to spustíme:

hello
**********
**********
* Python *
**********
**********

Zapíšme teraz presné kroky, ktoré sa vykonajú pri volaní funkcie:

  1. preruší sa vykonávanie práve bežiaceho programu (Python si presne zapamätá miesto, kde sa to stalo)

  2. skočí sa na začiatok volanej funkcie

  3. postupne sa vykonajú všetky príkazy

  4. keď sa príde na koniec funkcie, zrealizuje sa návrat na zapamätané miesto, kde sa prerušilo vykonávanie programu a pokračuje sa vo vykonávaní ďalších príkazov za volaním funkcie

Pre volanie funkcie sú veľmi dôležité okrúhle zátvorky. Bez nich to už nie je volanie, ale len zisťovanie referencie na hodnotu, ktorá je priradená pre toto meno. Napr.

>>> vypis()
**********
**********
>>> vypis
<function vypis at 0x0205CB28>

Ak by sme namiesto volania funkcie takto zapísali len meno funkcie bez zátvoriek, ale v skripte (teda nie v interaktívnom režime), táto hodnota referencie by sa nevypísala, ale odignorovala. Toto býva dosť častá chyba začiatočníkov, ktorá sa ale ťažšie odhaľuje.

Ak zavoláme funkciu, ktorú sme ešte nedefinovali, Python vyhlási chybu, napr.

>>> vipis()
...
NameError: name 'vipis' is not defined

Samozrejme, že môžeme volať len definované funkcie.

>>> vypis()
**********
**********
>>> vypis = 'ahoj'
>>> vypis
'ahoj'
>>> vypis()
...
TypeError: 'str' object is not callable

Hodnotou premennej vypis je už teraz znakový reťazec, a ten sa „nedá zavolať“, t.j. nie je „callable“ (tento objekt nie je zavolateľný ako funkcia).

Funkcie kreslia do grafickej plochy

Napíšme teraz funkciu, ktorá do grafickej plochy na náhodnú pozíciu napíše nejaký text, napr. 'PYTHON':

def kresli_text():
    x = randrange(380)
    y = randrange(260)
    canvas.create_text(x, y, text='PYTHON')

Aby sme túto funkciu mohli zavolať, musí už exitovať randrange aj canvas, teda

import tkinter
from random import randrange

def kresli_text():
    x = randrange(380)
    y = randrange(260)
    canvas.create_text(x, y, text='PYTHON')

canvas = tkinter.Canvas()
canvas.pack()

for i in range(20):
    kresli_text()

V tejto malej ukážke vidíme tieto novinky:

  • v tele funkcie môžeme používať premenné, ktoré sú definované mimo tela funkcie (randrange a canvas) - neskôr uvidíme, že im budeme hovoriť globálne premenné

  • v tele funkcie sme do dvoch premenných x a y priradili nejaké hodnoty a ďalej sme ich používali v ďalšom príkaze funkcie - neskôr uvidíme, že im budeme hovoriť lokálne premenné

_images/05_01.png

Parametre funkcií

Hotové funkcie, s ktorými sme doteraz pracovali, napr. print() alebo random.randint(), mali aj parametre, vďaka čomu riešili rôzne úlohy. Parametre slúžia na to, aby sme mohli funkcii lepšie oznámiť, čo špecifické má urobiť: čo sa má vypísať, z akého intervalu má vygenerovať náhodné číslo, akú úsečku má nakresliť, prípadne akej farby, …

Parametre funkcie

Parametrom funkcie je dočasná premenná, ktorá vzniká pri volaní funkcie a prostredníctvom ktorej, môžeme do funkcie poslať nejakú hodnotu. Parametre funkcií definujeme počas definovania funkcie v hlavičke funkcie a ak ich je viac, oddeľujeme ich čiarkami:

def meno_funkcie(parameter):
    prikaz
    prikaz
    ...

Môžeme napríklad zapísať:

def vypis_hviezdiciek(pocet):
    print('*' * pocet)

V prvom riadku definície funkcie (hlavička funkcie) pribudla jedna premenná pocet - parameter. Táto premenná vznikne automaticky pri volaní funkcie, preto musíme pri volaní oznámiť hodnotu tohto parametra. Volanie zapíšeme:

>>> vypis_hviezdiciek(30)
******************************
>>> for i in range(1, 10):
        vypis_hviezdiciek(i)

*
**
***
****
*****
******
*******
********
*********

Pri volaní sa „skutočná hodnota“ priradí do parametra funkcie (premenná pocet).

Už predtým sme popísali mechanizmus volania funkcie, ale to sme ešte nepoznali parametre. Teraz doplníme tento postup o spracovanie parametrov. Najprv trochu terminológie:

  • pri definovaní funkcie v hlavičke funkcie uvádzame tzv. formálne parametre: sú to nové premenné, ktoré vzniknú až pri volaní funkcie

  • pri volaní funkcie musíme do zátvoriek zapísať hodnoty, ktoré sa stanú tzv. skutočnými parametrami: tieto hodnoty sa pri volaní priradia do formálnych parametrov

Mechanizmus volania vysvetlíme na volaní vypis_hviezdiciek(30):

  1. zapamätá sa návratová adresa volania

  2. vytvorí sa nová premenná pocet (formálny parameter) a priradí sa do nej hodnota skutočného parametra 30

  3. vykonajú sa všetky príkazy v definícii funkcie (telo funkcie)

  4. zrušia sa všetky premenné, ktoré vznikli počas behu funkcie

  5. riadenie sa vráti na miesto, kde bolo volanie funkcie

Zapíšme novú funkciu cerveny_kruh(), ktorá bude mať dva parametre: súradnice stredu kruhu. Funkcia nakreslí kruh s polomerom 20 s daným stredom:

def cerveny_kruh(x, y):
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')

a môžeme ju zavolať napr. takto:

import tkinter

canvas = tkinter.Canvas()
canvas.pack()
for x in range(30, 350, 25):
    for y in range(20, 240, 15):
        cerveny_kruh(x, y)
_images/05_02.png

Aj v tomto príklade si popíšme Mechanizmus volania funkcie:

  1. zapamätá sa návratová adresa volania

  2. vytvoria sa dve nové premenné x a y (formálne parametre) a priradia sa do nej hodnoty skutočných parametrov 30 a 20 (prvé volanie funkcie)

  3. vykonajú sa všetky príkazy v definícii funkcie (telo funkcie)

  4. zrušia sa všetky premenné, ktoré vznikli počas behu funkcie, teda x aj y

  5. riadenie sa vráti na miesto, kde bolo volanie funkcie

Už vieme, že priraďovací príkaz vytvára premennú a referenciou ju spojí s nejakou hodnotou. Premenné, ktoré vzniknú počas behu funkcie, sa stanú lokálnymi premennými: budú existovať len počas tohto behu a po skončení funkcie, sa automaticky zrušia. Aj parametre vznikajú pri štarte funkcie a zanikajú pri jej skončení: tieto premenné sú pre funkciu tiež lokálnymi premennými.

V nasledovnom príklade funkcie vypis_sucet() počítame a vypisujeme súčet čísel od 1 po zadané n:

def vypis_sucet(n):
    sucet = 1
    print(1, end=' ')
    for i in range(2, n + 1):
        sucet = sucet + i
        print('+', i, end=' ')
    print('=', sucet)

Pri volaní funkcie sa pre parameter n = 10 vypíše:

>>> vypis_sucet(10)
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55

Počas behu vzniknú 2 lokálne premenné (sucet a i) a jeden parameter, ktorý je pre funkciu tiež lokálnou premennou:

  • n vznikne pri štarte funkcie aj s hodnotou 10

  • sucet vznikne pri prvom priradení sucet = 1

  • i vznikne pri štarte for-cyklu

Po skončení behu funkcie sa všetky tieto premenné automaticky zrušia.

Pozrime sa na lokálne premenné, ktoré vznikajú vo funkcii nahodne_kruhy(n, farba):

def nahodne_kruhy(n, farba):
    for i in range(n):
        x = randrange(380)
        y = randrange(260)
        canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill=farba)

Napr.

import tkinter
from random import randrange

canvas = tkinter.Canvas()
canvas.pack()

nahodne_kruhy(100, 'red')
nahodne_kruhy(50, 'blue')
nahodne_kruhy(10, 'yellow')

Program postupne nakreslí na náhodné pozície 100 červených, 50 modrých a 10 žltých kruhov.

_images/05_03.png

Všimnite si vo funkcii nahodne_kruhy() tri lokálne premenné (i, x, y) a dva parametre (n, farba), ktoré sú pre funkciu tiež lokálne premenné.

Menný priestor

Aby sme lepšie pochopili ako naozaj fungujú lokálne premenné, musíme rozumieť, čo to je a ako funguje menný priestor (namespace). Najprv trochu ďalšej terminológie: všetky identifikátory v Pythone sú jedným z troch typov (Python má pre identifikátory 3 rôzne tabuľky mien):

  • štandardné, napr. int, print, …

    • hovorí sa tomu builtins

  • globálne - definujeme ich na najvyššej úrovni mimo funkcií, napr. funkcie vypis_hviezdiciek, vypis_sucet, nahodne_kruhy, alebo aj premenné randrange, tkinter, canvas (zrejme tkinter je referenciou na importovaný modul)

    • hovorí sa tomu main

  • lokálne - vznikajú počas behu funkcie

Tabuľka štandardných mien (builtins) je pre celý program len jedna, tiež tabuľka globálnych mien (main) je len jedna, ale každá funkcia má svoju „súkromnú“ lokálnu tabuľku mien, ktorá vznikne pri štarte (zavolaní) funkcie a zruší sa pri konci vykonávania funkcie.

Keď na nejakom mieste použijeme identifikátor, Python ho najprv hľadá (v tzv. menných priestoroch):

  • v lokálnej tabuľke mien, ak tam tento identifikátor nenájde, hľadá ho

  • v globálnej tabuľke mien, ak tam tento identifikátor nenájde, hľadá ho

  • v štandardnej tabuľke mien

Ak nenájde v žiadnej z týchto tabuliek, hlási chybu NameError: name 'identifikátor' is not defined.

Príkaz (štandardná funkcia) dir() vypíše tabuľku globálnych mien. Hoci pri štarte Pythonu by táto tabuľka mala byť prázdna, obsahuje niekoľko špeciálnych mien, ktoré začínajú aj končia znakmi '__':

>>> dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']

Keď teraz vytvoríme nejaké nové globálne mená, objavia sa aj v tejto globálnej tabuľke:

>>> premenna = 2015
>>> def funkcia():
        pass

>>> dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__',
 'funkcia', 'premenna']

Podobne sa vieme dostať aj k tabuľke štandardných mien (builtins):

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', ...

Takto sa vypíšu všetky preddefinované mená. Vidíme medzi nimi napr. 'int', 'print', 'range', 'str', …

S týmito tabuľkami súvisí aj príkaz na zrušenie premennej.

príkaz del

Príkazom del zrušíme identifikátor z tabuľky mien. Formát príkazu:

del premenná

Príkaz najprv zistí, v ktorej tabuľke sa identifikátor nachádza (najprv pozrie do lokálnej a keď tam nenájde, tak do globálnej tabuľky) a potom ho z tejto tabuľky vyhodí. Príkaz ale nefunguje pre štandardné mená.

Ukážme to na príklade: identifikátor print je menom štandardnej funkcie (v štandardnej tabuľke mien). Ak v priamom režime (čo je globálna úroveň mien) do premennej print priradíme nejakú hodnotu, toto meno vznikne v globálnej tabuľke:

>>> print('ahoj')
ahoj
>>> print=('ahoj')          # do print sme priradili nejaku hodnotu
>>> print
'ahoj'
>>> print('ahoj')
...
TypeError: 'str' object is not callable

Teraz už print nefunguje ako funkcia na výpis hodnôt, ale len ako obyčajná globálna premenná. Ale v štandardnej tabuľke mien print stále existuje, len je táto premenná prekrytá globálnym menom. Python predsa najprv prehľadáva globálnu tabuľku a až keď sa tam nenájde, hľadá sa v štandardnej tabuľke. A ako môžeme vrátiť funkčnosť štandardnej funkcie print? Stačí vymazať identifikátor z globálnej tabuľky:

>>> del print
>>> print('ahoj')
ahoj

Vymazaním globálneho mena print ostane definovaný len identifikátor v tabuľke štandardných mien, teda opäť začne fungovať funkcia na výpis hodnôt.

Pozrime sa teraz na prípad, keď sa v tele funkcie bude nachádzať volanie inej funkcie (tzv. vnorené volanie), napr.

def vypis_hviezdiciek(pocet):
    print('*' * pocet)

def trojuholnik(n):
    for i in range(1, n + 1):
        vypis_hviezdiciek(i)

Pri ich definovaní v globálnom mennom priestore vznikli dva identifikátory: vypis_hviezdiciek a trojuholnik. Zavoláme funkciu trojuholnik:

>>> trojuholnik(5)

Najprv sa pre túto funkciu vytvorí jej menný priestor (lokálna tabuľka mien) s dvomi lokálnymi premennými: n a i. Teraz pri každom (vnorenom) volaní vypis_hviezdiciek(i) sa pre túto funkciu:

  • vytvorí nový menný priestor s jedinou premennou pocet

  • vykoná sa príkaz print()

  • nakoniec sa zruší jej menný priestor, t.j. zanikne premenná pocet

Môžeme to odkrokovať pomocou http://www.pythontutor.com/visualize.html#mode=edit (zapneme voľbu Python 3.6):

  • najprv do editovacieho okna zapíšeme nejaký program, napr.

    najprv vložíme program
  • spustíme vizualizáciu pomocou tlačidla Visualize Execution a potom niekoľkokrát tlačíme tlačidlo Forward >

    po niekoľkých krokoch vizualizácie

Všimnite si, že v pravej časti tejto stránky sa postupne zobrazujú menné priestory (tu sa nazývajú frame):

  • najprv len globálny priestor s premennými vypis_hviezdiciek a trojuholnik

  • potom sa postupne objavujú a aj miznú lokálne priestory týchto dvoch funkcií - na obrázku vidíme oba tieto menné priestory tesne pred ukončením vykonávania funkcie trojuholnik s parametrom 2

Funkcie s návratovou hodnotou

Väčšina štandardných funkcií v Pythone na základe parametrov vráti nejakú hodnotu, napr.

>>> abs(-5.5)
5.5
>>> round(2.36, 1)
2.4

Funkcie, ktoré sme zatiaľ vytvárali my, takú možnosť nemali: niečo počítali, niečo vypisovali, ale žiadnu návratovú hodnotu nevytvárali. Aby funkcia mohla vrátiť nejakú hodnotu ako výsledok volania funkcie, musí sa v jej tele objaviť príkaz return, napr.

def meno(parametre):
    prikaz
    prikaz
    ...
    return hodnota                  # takato funkcia bude vracat vyslednu hodnotu

Príkazom return sa ukončí výpočet funkcie (zruší sa jej menný priestor) a uvedená hodnota sa stáva výsledkom funkcie, napr.

def eura_na_koruny(eura):                   # prepočítanie na české koruny
    koruny = round(eura * 25.8, 2)
    return koruny

môžeme otestovať:

>>> print('mas', 123, 'euro, co je', eura_na_koruny(123), 'ceskych korun')
mas 123 euro, co je 3173.4 ceskych korun

Niekedy potrebujeme návratovú hodnotu počítať nejakým cyklom, napr. nasledovná funkcia počíta súčet čísel od 1 do n:

def suma(n):
    vysledok = 0
    while n > 0:
        vysledok += n
        n -= 1
    return vysledok

Zároveň vidíme, že formálny parameter (je to predsa lokálna premenná) môžeme v tele funkcie modifikovať.

Už sme videli, že rozlišujeme dva typy funkcií:

  • také, ktoré niečo robia (napr. vypisujú, kreslia, …), ale nevracajú návratovú hodnotu (neobsahujú return s nejakou hodnotou)

  • také, ktoré niečo vypočítajú a vrátia nejakú výslednú hodnotu - musia obsahovať return s návratovou hodnotou

Ďalej ukážeme, že rôzne funkcie môžu vracať hodnoty rôznych typov. Najprv číselné funkcie.

Výsledkom funkcie je číslo

Nasledovná funkcia počíta n-tú mocninu dvojky a tento výsledok ešte zníži o 1:

def pocitaj(n):
    return 2 ** n - 1

Zrejme výsledkom je vždy len číslo.

Ak chceme funkciu otestovať, buď ju spustíme s konkrétnym parametrom, alebo napíšeme cyklus, ktorý našu funkciu spustí s konkrétnymi hodnotami (niekedy na testovanie píšeme ďalšiu testovaciu funkciu, ktorá nerobí nič iné, „len“ testuje funkciu pre rôzne hodnoty a porovnáva ich s očakávanými výsledkami), napr.

>>> pocitaj(5)
31
>>> for i in 1, 2, 3, 8, 10, 16, 20, 32:
        print(f'pocitaj({i}) = {pocitaj(i)}')

pocitaj(1) = 1
pocitaj(2) = 3
pocitaj(3) = 7
pocitaj(8) = 255
pocitaj(10) = 1023
pocitaj(16) = 65535
pocitaj(20) = 1048575
pocitaj(32) = 4294967295

Ďalšia funkcia zisťuje dĺžku (počet znakov) zadaného reťazca. Využíva to, že for-cyklus vie prejsť všetky znaky reťazca a s každým môže niečo urobiť, napr. zvýšiť počítadlo o 1:

def dlzka(retazec):
    pocet = 0
    for znak in retazec:
        pocet += 1
    return pocet

Otestujeme:

>>> dlzka('Python')
6
>>> dlzka(10000 * 'ab')
20000

Výsledkom funkcie je logická hodnota

Funkcie môžu vracať aj hodnoty iných typov, napr.

def parne(n):
    return n % 2 == 0

vráti True alebo False podľa toho či je n párne (zvyšok po delení 2 bol 0), vtedy vráti True, alebo nepárne (zvyšok po delení 2 nebol 0) a vráti False. Túto istú funkciu môžeme zapísať aj tak, aby bolo lepšie vidieť tieto dve rôzne návratové hodnoty:

def parne(n):
    if n % 2 == 0:
        return True
    else:
        return False

Hoci táto verzia robí presne to isté ako predchádzajúca, skúsení programátori radšej používajú kratšiu prvú verziu. Keď chceme túto funkciu otestovať, môžeme zapísať:

>>> parne(10)
True
>>> parne(11)
False
>>> for i in range(20, 30):
        print(i, parne(i))

20 True
21 False
22 True
23 False
24 True
25 False
26 True
27 False
28 True
29 False

Výsledkom funkcie je reťazec

Napíšme funkciu, ktorá vráti nejaký reťazec v závislosti od hodnoty parametra:

def farba(ix):
    if ix == 0:
        return 'red'
    elif ix == 1:
        return 'blue'
    else:
        return 'yellow'

Funkcia vráti buď červenú, alebo modrú, alebo žltú farbu v závislosti od hodnoty parametra.

Opäť by ju bolo dobre najprv otestovať, napr.

>>> for i in range(6):
        print(i, farba(i))

0 red
1 blue
2 yellow
3 yellow
4 yellow
5 yellow

Uvedomte si, prečo ju môžeme zapísať aj takto bez else vetiev:

def farba(ix):
    if ix == 0:
        return 'red'
    if ix == 1:
        return 'blue'
    return 'yellow'

V takýchto prípadoch je na vás, ktorý zápis použijete, ktorý z nich sa vám zdá čitateľnejší.

Typy parametrov a typ výsledku

Python nekontroluje typy parametrov, ale kontroluje, čo sa s nimi robí vo funkcii. Napr. funkcia:

def pocitaj(x):
    return 2 * x + 1

bude fungovať pre čísla, ale pre reťazec spadne:

>>> pocitaj(5)
11
>>> pocitaj('a')
...
TypeError: Can't convert 'int' object to str implicitly

V tele funkcie ale môžeme kontrolovať typ parametra, napr. takto

def pocitaj(x):
    if type(x) == str:
        return 2 * x + '1'
    else:
        return 2 * x + 1

a potom volanie

>>> pocitaj(5)
11
>>> pocitaj('a')
'aa1'

Neskôr sa naučíme testovať typ nejakých hodnôt správnejším spôsobom, ale zatiaľ nám bude stačiť, keď to budeme riešiť takto jednoducho.

Napriek tomuto niektoré funkcie môžu fungovať rôzne pre rôzne typy, napr.

def urob(a, b):
    return 2 * a + 3 * b

niekedy funguje pre čísla aj pre reťazce. Otestujte.

Grafické funkcie

Zadefinujeme funkcie, pomocou ktorých sa nakreslí 5000 náhodných farebných bodiek, ktoré budú zafarbené podľa nejakých pravidiel:

import tkinter
from random import randint
from math import sqrt

def vzd(x1, y1, x2, y2):
    return sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)

def kresli_bodku(x, y, farba):
    canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill=farba, width=0)

def farebne_bodky(pocet):
    for i in range(pocet):
        x = randint(10, 290)
        y = randint(10, 290)
        if vzd(x, y, 150, 150) > 100:
            kresli_bodku(x, y, 'navy')
        elif vzd(x, y, 230, 150) > 100:
            kresli_bodku(x, y, 'red')
        else:
            kresli_bodku(x, y, 'yellow')

canvas = tkinter.Canvas(width=300, height=300)
canvas.pack()

farebne_bodky(5000)

Funkcia vzd() počíta vzdialenosť dvoch bodov (x1, y1) a (x2, y2) v rovine - tu sa použil známy vzorec z matematiky. Táto funkcia nič nevypisuje, ale vracia číselnú hodnotu (desatinné číslo). Ďalšia funkcia kresli_bodku() nič nevracia, ale vykreslí v grafickej ploche malý kruh s polomerom 5, ktorý je zafarbený zadanou farbou. Tretia funkcia farebne_bodky() dostáva ako parameter počet bodiek, ktoré má nakresliť: funkcia na náhodné pozície nakreslí príslušný počet bodiek, pričom tie, ktoré sú od bodu (150, 150) vzdialené viac ako 100, budú tmavomodré (farba 'navy'), tie, ktoré sú od bodu (230, 150) vzdialené viac ako 100, budú červené a všetku ostatné budú žlté. Všimnite si, že sme za definíciami všetkých funkcií napísali samotný program, ktorý využíva práve zadefinované funkcie. Po spustení dostávame približne takýto obrázok:

náhodne generované bodky

Náhradná hodnota parametra

Naučíme sa zadefinovať parametre funkcie tak, aby sme pri volaní nemuseli uviesť všetky hodnoty skutočných parametrov, ale niektoré sa automaticky dosadia, tzv. náhradnou hodnotou (default), napr.

def kresli_bodku(x, y, farba='red', r=5):
    canvas.create_oval(x - r, y - r, x + r, y + r, fill=farba, width=0)

V hlavičke funkcie môžeme k niektorým parametrom uviesť náhradnú hodnotu (vyzerá to ako priradenie). V tomto prípade to označuje, že ak tomuto formálnemu parametru nebude zodpovedať skutočný parameter, dosadí sa práve táto náhradná hodnota. Pritom musí platiť, že keď nejakému parametru v definícii funkcie určíte, že má náhradnú hodnotu, tak náhradnú hodnotu musíte zadať aj všetkým ďalším formálnym parametrom, ktoré sa nachádzajú v zozname parametrov za ním (ak sme zadefinovali náhradnú hodnotu pre parameter farba, musíme nejakú zadefinovať aj pre parameter r).

Teraz môžeme zapísať aj takéto volania tejto funkcie:

kresli_bodku(100, 200, 'blue', 3)    # farba bude 'blue' a r bude 3
kresli_bodku(150, 250, 'blue')       # farba bude 'blue' a r bude 5
kresli_bodku(200, 200)               # farba bude 'red'  a r bude 5

Parametre volané menom

Funkcia kresli_bodku má štyri parametre: x, y, farba a r.

Python umožňuje funkcie s parametrami volať tak, že skutočné parametre neurčujeme pozične (prvému skutočnému zodpovedá prvý formálny, druhému druhý, atď.) ale priamo pri volaní uvedieme meno parametra. Takto môžeme určiť hodnotu ľubovoľnému parametru. Napr. všetky tieto volania sú korektné:

kresli_bodku(10, 20, r=10)
kresli_bodku(farba='green', x=10, y=20)
kresli_bodku(r=7, farba='yellow', y=20, x=30)

Samozrejme aj pri takomto volaní môžeme vynechať len tie parametre, ktoré majú určenú náhradnú hodnotu, všetky ostatné parametre sa musia v nejakom poradí objaviť v zozname skutočných parametrov.

Farebný model RGB

Keďže už vieme vytvárať reťazce so šestnástkovým zápisom čísel (napr. 'f{číslo:02x}' alebo '{:02x}'.format(číslo)) zapíšeme funkciu rgb(), ktorá bude vytvárať farby pomocou RGB-modelu:

def rgb(r, g, b):
    return f'#{r:02x}{g:02x}{b:02x}'

otestujme:

>>> rgb(255, 255, 0)
'#ffff00'
>>> rgb(0, 100, 0)
'#006400'

Funkciu rgb() môžeme využiť napr. na kreslenie farebných štvorcov:

import tkinter

canvas = tkinter.Canvas()
canvas.pack()

def stvorec(strana, x, y, farba=''):
    canvas.create_rectangle(x, y, x + strana, y + strana, fill=farba)

for i in range(10):
    stvorec(30, i*30, 10, rgb(100+16*i, 0, 0))
    stvorec(30, i*30, 50, rgb(100+16*i, 0, 255-26*i))
    stvorec(30, i*30, 90, rgb(26*i, 26*i, 26*i))
    stvorec(30, i*30, 130, rgb(0, 26*i, 26*i))

Tento program nakreslí takýchto 40 zafarbených štvorcov:

miešanie farieb

Náhodné farby

Ak potrebujeme generovať náhodnú farbu, ale stačí nám iba jedna z dvoch možností, môžeme to urobiť napr. takto:

def nahodna2_farba():
    if random.randrange(2):
        return 'blue'
    return 'red'

Podobne by sa zapísala funkcia, ktorá generuje náhodnú farbu jednu z troch a pod.

Ak ale chceme úplne náhodnú farbu z celej množiny všetkých farieb, využijeme RGB-model napr. takto

def rgb(r, g, b):
    return f'#{r:02x}{g:02x}{b:02x}'

def nahodna_farba():
    return rgb(random.randrange(256), random.randrange(256), random.randrange(256))

Keďže takto sa vlastne vygeneruje náhodné šestnástkové šesťciferné číslo, toto isté vieme zapísať aj takto:

def nahodna_farba():
    return f'#{random.randrange(256*256*256):06x}'     # co je aj 256**3

Môžeme vygenerovať štvorcovú sieť náhodných farieb:

import tkinter
from random import randrange

canvas = tkinter.Canvas()
canvas.pack()

for y in range(0, 230, 30):
    for x in range(0, 350, 30):
        stvorec(26, x, y, nahodna_farba())

Dostaneme nejaký takýto obrázok:

náhodné farby

Niekoľko užitočných matematických funkcií

Na záver ukážeme sériu zaujímavých matematických funkcií. Začneme s vypis_delitele(cislo), ktorá do jedného riadka vypíše všetky delitele daného čísla:

def vypis_delitele(cislo):
    for i in range(1, n + 1):
        if cislo % i == 0:
            print(i, end=' ')
    print()
>>> vypis_delitele(24)
1 2 3 4 6 8 12 24

Ďalšia funkcia sucet_delitelov(cislo) tieto delitele nevypisuje, ale vráti ich súčet (pomocou return):

sucet_delitelov(cislo):
    vysl = 0
    for i in range(1, n + 1):
        if cislo % i == 0:
            vysl += i
    return vysl
>>> print('súčet deliteľov 24 je', sucet_delitelov(24))
súčet deliteľov 24 je 60

Funkcia je_dokonale(cislo) pomocou funkcie sucet_delitelov() zistí, či je dané číslo dokonalé, t.j. že súčet všetkých menších deliteľov ako samotné číslo sa rovná samotnému číslu. Napr. delitele čísla 6 (menšie ako 6) sú 1, 2, 3. Ich súčet je 6. Preto je číslo 6 dokonalé. Funkcia nič nevypisuje, ale vracia (pomocou return) True alebo False.

def je_dokonale(cislo)
    return sucet_delitelov(cislo) == 2 * cislo
>>> je_dokonale(6)
True
>>> je_dokonale(24)
False

Ďalšia funkcia vsetky_dokonale(od, do) vypíše všetky dokonalé čísla v danom intervale <od, do>. Táto funkcia využije funkciu je_dokonale():

def vsetky_dokonale(od, do):
    for cislo in range(od, do + 1):
        if je_dokonale(cislo):
            print(cislo, 'je dokonalé')
>>> vsetky_dokonale(1, 30)
6 je dokonalé
28 je dokonalé

Zapíšeme funkciu nsd(a, b), ktorá počíta najväčší spoločný deliteľ dvoch čísel. Použijeme tzv. Euklidov algoritmus, ktorý už pred vyše 2 tisíc rokmi popísal starogrécky matematik Euklides:

def nsd(a, b):
    while a != b:
        if a > b:
            a = a - b
        else:
            b = b - a
    return a
>>> nsd(60, 18)
6
>>> nsd(100000000, 1)
1

Iste ste si všimli, že volanie nsd(100000000, 1) trvá niekoľko sekúnd. Zrejme preto, lebo while-cyklus 100000000-krát odpočíta 1. Pritom si stačí uvedomiť, že takýto cyklus:

while a > b:
    a = a - b

pre kladné čísla a a n urobí to isté ako:

a = a % b

teda zvyšok po delení. Funkciu nsd môžeme teraz výrazne vylepšiť:

def nsd(a, b):
    while b != 0:
        a, b = b, a % b
    return a

Odkrokujte tento algoritmus pomocou stránky http://www.pythontutor.com/visualize.html#mode=edit.

Ďalšia funkcia pocet_delitelov(cislo) pre dané číslo zistí počet všetkých deliteľov. Napr. delitele čísla 61, 2, 3, 6, preto funkcia vráti 4. Funkcia nič nevypisuje, ale vracia (pomocou return) celé číslo:

def pocet_delitelov(cislo):
    vysl = 0
    for i in range(1, n + 1):
        if cislo % i == 0:
            vysl += 1
    return vysl
>>> pocet_delitelov(6)
4
>>> pocet_delitelov(17)
2

Teraz funkcia je_prvocislo(cislo) pomocou funkcie pocet_delitelov() veľmi jednoducho zistí (vráti True alebo False), či je to prvočíslo (je deliteľné len 1 a samým sebou):

def je_prvocislo(cislo):
    return pocet_delitelov(cislo) == 2
>>> je_prvocislo(6)
False
>>> je_prvocislo(17)
True

Posledná funkcia vsetky_prvocisla(od, do) vypíše do jedného riadka všetky prvočísla v danom intervale:

def vsetky_prvocisla(od, do):
    for cislo in range(od, do + 1):
        if je_prvocislo(cislo):
            print(cislo, end=' ')
    print()
>>> vsetky_prvocisla(1, 30)
2 3 5 7 11 13 17 19 23 29

Aj tento algoritmus odkrokujte pomocou stránky http://www.pythontutor.com/visualize.html#mode=edit.




Cvičenia

L.I.S.T.

  1. Napíš funkciu priemer(a, b), ktorá vypočíta priemer dvoch zadaných čísel. Funkcia nič nevypisuje, ale pomocou return vráti vypočítanú hodnotu. Otestuj ju s rôznymi hodnotami parametrov.

    • napr.

      >>> priemer(1, 4)
      2.5
      >>> priemer(3.14, 31.4)
      17.27
      
  2. Napíš funkciu max(a, b, c), ktorá vráti najväčšiu zo zadaných hodnôt. Funkcia nič nevypisuje, ale pomocou return vráti vypočítanú hodnotu. Otestuj ju s rôznymi hodnotami parametrov. Nepoužívaj štandardnú funkciu max.

    • napr.

      >>> max(1, 4, -3)
      4
      >>> max(-7.2, 0, -3)
      0
      
  3. Napíš funkciu faktorial(n), ktorá vypočíta faktoriál čísla n. Funkcia nič nevypisuje, ale pomocou return vráti vypočítanú hodnotu. Otestuj ju s rôznymi hodnotami parametra.

    • napr.

      >>> faktorial(6)
      720
      >>> faktorial(0)
      1
      
  4. Napíš funkciu kombinacne_cislo(n, k), ktorá počíta kombinačné číslo n nad k. Pri výpočte čísla nepočítaj faktoriály, ale minimalizuj počet násobení. Funkcia nič nevypisuje, ale pomocou return vráti vypočítanú hodnotu.

    • napr.

      >>> kombinacne_cislo(100, 2)
      4950
      >>> (100 * 99) // (1 * 2)
      4950
      >>> kombinacne_cislo(1000, 3)
      166167000
      >>> (1000 * 999  * 998) // (1 * 2 * 3)
      166167000
      
  5. Napíš funkciu fibonacci(n), ktorá vypočíta n-té fibonacciho číslo.

    • napr.

      >>> for i in range(8):
              print(i, fibonacci(i))
      
      0 0
      1 1
      2 1
      3 2
      4 3
      5 5
      6 8
      7 13
      
  6. Napíš funkciu tretia_odmocnina(cislo, eps=0.001), ktorá podobným algoritmom, ako sa počítala druhá odmocnina delením intervalu na polovicu v 4. prednáške, vypočíta tretiu odmocninu zadaného čísla. Druhý parameter eps označuje presnosť výpočtu.

    • otestujte napr. takto:

      for i in range(2, 5):
          print(i, tretia_odmocnina(i, 1e-12), i ** (1/3))
      
      2 1.2599210498947286 1.2599210498948732
      3 1.4422495703074674 1.4422495703074083
      4 1.5874010519682997 1.5874010519681994
      
  7. Napíš funkciu vyhod_medzery(text), ktorá zo zadaného textu vyhodí všetky medzery. Nepoužívajte žiadne funkcie ani operácie s reťazcami, ktoré sme sa ešte neučili. Funkcia nič nevypisuje, ale pomocou return vráti nový reťazec. Otestuj ju s rôznymi hodnotami parametrov.

    • napr.

      >>> vyhod_medzery('  mám   rád Python ')
      'mámrádPython'
      >>> vyhod_medzery('      ')
      ''
      
  8. Napíš funkcie stvorec(x, y, a, farba), trojuholnik(x, y, a, farba) a domcek(x, y, a=50, farba1='blue', farba2='red'). Funkcia stvorec nakreslí farebný štvorec s ľavým horným vrcholom v (x, y) a stranou a. Funkcia trojuholnik nakreslí rovnoramenný trojuholník s ľavým dolným vrcholom v (x, y) so stranou aj výškou a. Funkcia domcek nakreslí domček pomocou štvorca a trojuholníka (trojuholník leží na štvorci).

    • otestuj vykreslením 10 domčekov na náhodných pozíciách, napr.

      canvas = ...
      domcek(100, 200, 50)
      ...
      
  9. Napíš funkciu karticka(x, y, text), ktorá nakreslí bledošedý obdĺžnik a do jeho stredu vypíše zadaný text. Stred kartičky má súradnice (x, y) a jej strany majú dĺžky 100 a 40. Font písma môže byť napr. 'arial 14'.

    • otestuj náhodným vygenerovaním 10 kartičiek, napr.

      for i in range(10):
          karticka(randint(50, 300), randint(50, 200), 'Python')
      
  10. Napíš funkciu stvorcova_siet(m, n, velkost), ktorá pomocou create_line vykreslí štvorcovú sieť s m riadkami a n stĺpcami. Veľkosť štvorčeka siete nech je velkost.

    • otestuj napr.

      >>> stvorcova_siet(8, 12, 30)
      
  11. Napíš funkciu text_s_tienom(x, y, text), ktorá na zadané súradnice vypíše zadaný text nejakým zväčšeným fontom. Tento text vypíše dvakrát, pričom prvýkrát je to posunuté oproti (x, y) o nejakú malú vzdialenosť a text je svetlošedý. Druhýkrát je text vypísaný napr. modrou farbou.

    • otestuj napr.

      >>> text_s_tienom(50, 100, 'Programovanie')
      
  12. Vektor si môžeme predstaviť ako úsečku, ktorá je daná jedným vrcholom (x, y), dĺžkou a uhlom. Úsečku nakreslíme tak, aby mala tvar šípky (do create_line pridáme pomenovaný parameter arrow='last'). Napíš funkciu vektor(x, y, dlzka, uhol).

    • otestuj napr takto:

      for i in range(10):
          vektor(randint(50, 300), randint(50, 200), randint(10, 100), randint(0, 359))
      
  13. Napíš funkciu dvojkova_sustava(cislo), ktorá zadané číslo vráti ako znakový reťazec. Tento reťazec reprezentuje toto číslo v dvojkovej sústave. Úlohu rieš pomocou while-cyklu, v ktorom sa zistí posledná cifra v dvojkovom zápise (zvyšok po delení 2) a samotné číslo sa vydelí 2. Toto sa opakuje, kým je číslo väčšie ako 0.

    • napr.

      >>> dvojkova_sustava(725)
      '1011010101'
      >>> dvojkova_sustava(0)
      '0'
      
  14. Napíš funkciu slnko(n, x, y), ktorá nakreslí slnko ako n lúčov (hrubšie žlté úsečky, ktoré vychádzajú zo stredu (x, y) a majú dĺžku 70) a veľký žltý kruh so stredom (x, y) a polomerom 50.

    • otestuj napr.

      >>> slnko(10, 100, 80)
      >>> slnko(20, 180, 120)
      
  15. Napíš funkciu vybodkuj_usecku(x1, y1, x2, y2, n), ktorá nakreslí úsečku z bodu (x1, y1) do bodu (x2, y2). Túto úsečku nekreslí pomocou create_line, ale pomocou n bodiek, t.j. malých kruhov s polomerom napr. 3. n je minimálne 2 a vtedy sa nakreslia len dve bodky v koncových vrcholoch úsečky. Pre kontrolu môžete najprv vykresliť originálnu úsečku šedou farbou.

    • otestuj napr.

      >>> canvas.create_line(100, 80, 180, 120, fill='light gray')
      >>> vybodkuj_usecku(100, 80, 180, 120, 20)
      
  16. Napíš funkciu otoceny_stvorec(strana, stred_x, stred_y, uhol=0), ktorá pomocou čiar (create_line) nakreslí štvorec so stranou strana a so stredom (stred_x, stred_y). Tento štvorec bude otočený o zadaný uhol. Zrejme vrcholy takéhoto štvorca ležia na kružnici s polomerom strana * 2 ** .5 / 2 a uhol treba ešte otočiť o 45, aby otočený štvorec o uhol 0 sa presne prekrýval s obyčajným neotočeným štvorcom (create_rectangle).

    • otestuj neotočený aj otočený štvorec napr.

      canvas.create_rectangle(40, 70, 140, 170, outline='red')
      otoceny_stvorec(100, 90, 120, 0)
      
      canvas.create_rectangle(190, 70, 290, 170, outline='red')
      otoceny_stvorec(100, 240, 120, 10)
      
    • alebo

      uhol = 0
      for x in range(20, 370, 20):
          otoceny_stvorec(50, x, 130, uhol)
          uhol += 5