Skip to content

Add lesson on interfaces #637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions lessons/beginners/interfaces/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Rozhraní

Už víš že funkce ti umožňují kousek kódu:

* použít (zavolat) na více místech v programu, i když definice je jen jedna,
* vyčlenit, aby detail (jako načtení čísla od uživatele) „nezavazel“ ve větším
programu, který tak může být přehlednější, a
* pojmenovat, aby bylo jasné co kód dělá i bez toho, abys musel{{a}} číst
samotné tělo funkce.

Další výhoda funkce je, že ji můžeš jednoduše vyměnit za jinou,
lepší funkci – pokud má ta lepší funkce stejné *rozhraní* (angl. *interface*).

Aby se ti líp představovalo, o čem budeme povídat, představ si elektrickou
zásuvku ve zdi.
Do takové zásuvky můžeš zapojit počítač, lampu, nabíječku na mobil, vysavač,
nebo rádio.
Zásuvka poskytuje elektrický proud; je jedno, jak ho použiješ.
Stejně tak je jedno jestli je „druhý konec“ zásuvky připojený k solárnímu
panelu nebo k atomové elektrárně.
Zásuvka poskytuje elektrický proud, a jsou u ní důležité určité parametry
(tvar, napětí, frekvence, maximální proud) na kterých se obě strany,
poskytovatel proudu i spotřebič, shodly.


# Funkce jako rozhraní

Podívej se na tuhle hlavičku funkce.
Víš z ní, co ta funkce dělá a jak ji použít?

```python
def ano_nebo_ne(otazka):
"""Zeptá se uživatele na otázku a vrátí True nebo False dle odpovědi"""
```

Podobnou funkci už jsi napsala; víš že „vevnitř“ volá `input` a ptá se na
příkazové řádce.

Co kdybys ale měla následující funkci?

```python
def ano_nebo_ne(otazka):
"""Ukáže tlačítka "Ano" a "Ne" a až uživatel jedno zmáčkne, vrátí True
nebo False dle stisknutého tlačítka."""
```

<img src="{{ static('yn.png') }}" alt="Screenshot s tlačítky Ano a Ne" style="display:block;float:right;">

Když zavoláš tuhle funkci, `ano_nebo_ne('Chutná ti čokoláda?')`, ukáže se
okýnko se dvěma tlačítky.
Když uživatel jedno zmáčkne, funkce vrátí True nebo False.

Z hlediska programu se nic nemění: jediné co se změní je *definice funkce*;
volání je pak stejné jako dřív.


# Vyzkoušej si to!

Najdi nějaký svůj program, který používá `ano_nebo_ne`, případně jen `print`
a `input`.

Stáhni si modul <a href="{{ static('tkui.py') }}"><code>tkui.py</code></a>
do adresáře se svým programem.
Naimportuj z něho funkce, které potřebuješ.
Jsou k dispozici čtyři:

```python
from tkui import input, nacti_cislo, ano_nebo_ne, print
```

Tento import *přepíše* vestavěné funkce `input` a `print` variantami,
které mají (téměř) stejné rozhraní – jen dělají něco trochu jinak.

Případné vlastní definice funkcí `nacti_cislo` a `ano_nebo_ne` pak z programu
vyndej, aby se použily ty naimportované.

Program by měl fungovat stejně jako dřív!

Je to tím, že tyto funkce mají stejné rozhraní jako jejich dřívější protějšky,
tedy:

* jméno, kterým se funkce volá,
* argumenty, které bere (např. `input` bere otázku jako řetězec; `print`
může bere více argumentů k vypsání), a
* návratovou hodnotu, se kterou program pracuje dál (např `input` vrací
řetězec; u `print` nevrací nic smysluplného).

Většina z těchto informací je přímo v hlavičce funkce.
Ty ostatní je dobré popsat v dokumentačním řetězci, aby ten, kdo chce funkci
použít, věděl jako na to.


# Je to dobrý nápad?

Modul `tkui` je jen ilustrační. Nedoporučuju ho používat.

Příkazová řádka je dělaná tak, aby byla užitečná pro programátory.
Až se naučíš základy a vytvoříš nějaký skvělý program, přijde čas
k logice (tzv. *backendu*) přidat část, která bude lépe použitelná pro
uživatele – tedy okýnko nebo webovou stránku (tzv. *frontend*).

Udělat hezké a funkční *uživatelské* rozhraní je ovšem většinou celkem složité,
a často se dělá až potom, co jsou samotné „vnitřnosti“ funkční a otestované.
Doporučuju postupovat stejně, když se programování učíš: zůstaň u základních
`print` a `input`, dokud nezvládneš samotné programování.
A pak se můžeš naučit něco nového!

Co si ale z této lekce odnes je koncept rozhraní: při zachování několika
informací z hlavičky je možné vyměnit funkci za něco úplně jiného.
A stejně tak je možné jednu funkci (třeba `input`) volat ze spousty různých
programů, pokud znáš její rozhraní.
4 changes: 4 additions & 0 deletions lessons/beginners/interfaces/info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
title: Rozhraní
style: md
attribution: Pro PyLadies Brno napsal Petr Viktorin, 2020.
license: cc-by-sa-40
146 changes: 146 additions & 0 deletions lessons/beginners/interfaces/static/tkui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Modul s funkcemi pro okýnkové otázky a odpovědi:

input(otazka) -> str

nacti_cislo(otazka) -> int

ano_nebo_ne(otazka) -> bool

print(argument0, argument1, argument2, ..., argument_n, sep='')

"""

from tkinter import Tk, LEFT, RIGHT, BOTTOM, W
from tkinter.ttk import Label, Button, Spinbox, Entry


# Následující kód používá několik pokročilejších technik.
# Není důležité *jak* je kód napsaný, ale že je možné ho napsat – a že daná
# funkce má dané rozhraní :)
#
# Mimochodem; opravdové funkce input a print jsou ještě složitější; viz:
# https://github.com/python/cpython/blob/ce105541f/Python/bltinmodule.c#L1931
# https://github.com/python/cpython/blob/ce105541f/Python/bltinmodule.c#L1827

# Funkce používají modul tkinter, který je zabudovaný v Pythonu, ale okýnka
# s ním vytvořená nevypadají příliš profesionálně.
# Budeš-li chtít začít psát "okýnkové" programy, doporučuji začít rovnou
# s knihovnou jako Qt nebo GTK.
# Na Qt máme mimochodem lekci v pokročilém kurzu:
# viz https://naucse.python.cz/course/mi-pyt/intro/pyqt/


def input(otazka='odpověz'):
"""Zeptá se uživatele na otázku a vrátí odpověď jako řetězec."""
root = Tk()
root.title(otazka)

button = Button(root, text="OK", command=root.quit)
button.pack(side=RIGHT)

entry = Entry(root)
entry.pack(side=LEFT)

root.mainloop()

value = entry.get()
root.destroy()

return value


def nacti_cislo(otazka='Zadej číslo'):
"""Zeptá se uživatele na otázku a vrátí odpověď jako celé číslo."""
root = Tk()
root.title(otazka)

entry = Spinbox(root, from_=0, to=100)
entry.set('0')
entry.pack(side=LEFT)

# Předbíháme: vnořená funkce může přistupovat
# k proměnným "entry" a "root", které jsou
# lokální pro "vnější" funkci (nacti_cislo)

def ok_pressed():
text = entry.get()
try:
value = int(text)
except ValueError:
entry.set('sem zadej číslo!')
else:
root.quit()

button = Button(root, text="OK", command=ok_pressed)
button.pack(side=RIGHT)

root.mainloop()

value = int(entry.get())
root.destroy()

return value


def ano_nebo_ne(otazka='Ano nebo ne?'):
"""Dá uživateli na výběr Ano/Ne a vrátí odpověď True nebo False."""
root = Tk()
root.title(otazka)

value = False

# Předbíháme: "nonlocal" umožňuje *měnit*
# lokální proměnnou z vnější funkce.

def yes():
nonlocal value
value = True
root.quit()

def no():
nonlocal value
value = False
root.quit()

button = Button(root, text="Ano", command=yes)
button.pack(side=LEFT)

button = Button(root, text="Ne", command=no)
button.pack(side=RIGHT)

root.mainloop()
root.destroy()

return value



# Předbíháme: hvězdička v *args umožní že "print" bere proměnný
# počet argumentů; přes "args" se potom dá projít příkazem "for"
def print(*args, sep=' ', end='', file=None, flush=False):
"""Zobrazí dané argumenty."""
root = Tk()
root.title('print')

str_args = ''
for arg in args:
str_args = str_args + sep + str(arg)

button = Label(root, text=str_args[len(sep):] + end)
button.pack(anchor=W)

button = Button(root, text="OK", command=root.quit)
button.pack(side=BOTTOM)

root.bind('<Return>', (lambda e: root.quit()))
root.mainloop()
root.destroy()


# A tady je trik, jak kousek kódu nespustit když se modul importuje.
# Pro opravdové programy ale doporučuji spouštěcí modul, viz kurz.
if __name__ == '__main__':
print(input())
print(nacti_cislo())
print(ano_nebo_ne())
print('a', 'b', 'c', sep='; ', end='-')
Binary file added lessons/beginners/interfaces/static/yn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.