V minulém díle jste viděli...¶

Proč moduly¶

Řekli jsme si, proč je výhodné používat moduly: Moduly vznikly z potřeby uchovávat definice funkcí a proměnných tak, aby byly dostupné i po ukončení běhu Python interpreteru. Když ukončíte a znovu spustíte Python interpreter, ztratíte všechny definice, které jste vytvořili. Pro psaní delších programů je tak lepší využít externích souborů a tím vytvořit skripty. A jak se váš program stává komplexnějším, může být výhodné rozdělit ho do několika souborů pro snazší údržbu a přehlednost.

import¶

Seznámili jsme se podrobně s příkazem import a s tím, co se děje na pozadí

moduly standardní knihovny¶

Modul Popis
math matematické funkce pro pokročilé výpočty
cmath funkce podobné, jako v modulu math. Umí však počítat v komplexním prostoru
random slouží k generování pseudo-náhodných čísel
sys obsahuje funkce a hodnoty, ktere jsou vyuzivany interpreterem nebo ho ovlivnuji
os funkce pro interakci se systémovým prostředím
pprint hezke a prehledne vypisovani slozitejsich struktur
collections poskytuje alternativní datové typy k vestavěným typům
itertools funkce pro vytváření efektivních iterátorů
csv poskytuje funkcionalitu pro práci s formátem CSV
json umožňuje práci s formátem JSON
argparse usnadnuje praci s parametry skriptu

Modulární stavba programu¶

Již minulé cvičení jsme si řekli, proč je výhodné používat moduly a seznámili jsme se několika z nich ze standardní knihovny.

Dnes si ukážeme, jak si můžeme vytvořit moduly vlastní.

Moduly¶

Jak vytvořit modul¶

Modul je velice lehké vytvořit. Jedná se obyčejný textový soubor s příponou .py, který obsahuje definice funkcí a tříd, konstanty a ostatní výrazy v jazyce Python. Název tohoto souboru zároveň reprezentuje jméno modulu (bez přípony .py).

Jméno modulu je také automaticky přístupné v globální proměnné (string) __name__.

In [12]:
import math as matematika
print(matematika.__name__)
math

První modul¶

Pojďme vytvořit náš první modul. Vytvoříme soubor mymodule.py v aktuální složce a bude obsahovat jen jednu proměnnou,jednu funkci a jedno volání funkce print. Soubor si otevřete a prohlédněte.

Má následující obsah

myvariable = f'I am variable myvariable from module {__name__}'

def say_hello():
    print(f'Hello! I am function say_hello from module {__name__} ')

print(f'This is an executable statement from module {__name__}'}

Nyní tento modul můžeme importovat a pracovat s ním, podobně jako jinými moduly. To jsme se naučili minule:

In [13]:
import mymodule

print(mymodule.myvariable)

mymodule.say_hello()
This is an executable statement from module mymodule
I am variable myvariable from module mymodule
Hello! I am function say_hello from module mymodule 
In [1]:
# UKAZKA ... přepíšu si proměnnou
myvariable = 10
print(myvariable)

from mymodule import myvariable
print(myvariable)
10
This is an executable statement from module mymodule
I am variable myvariable from module mymodule

Poznámka: každý modul má vlastní soukromý "globální" namespace. Nemusíme se tak bát kolizí stejných jmen při importu více modulů. Pozor musíme dávat jen v případě importu *. Ale tím si přinejhorším překryjeme nějaká jména jen v našem kontextu.

Cesta pro hledání modulů¶

Mohla Vás napadnout otázka: Jak Python ví, kde se náš modul mymodule nachází?

Příkaz import při hledání modulů zhruba následuje tento postup:

  1. podívá se, jestli modul není součástí vestavěných modulů (jejich seznam je dostupný v rámci sys.builtin_module_names)
  2. Pokud ho nenalezne mezi vestavěnými moduly, hledá soubor mymodule.py v seznamu adresářů uloženém v proměnné sys.path

Poznámka: V těchto cestách krom souboru mymodule.py hledá i složku nazvanou mymodule obsahující soubor __init__.py. To je tzv python balíček. O tom více informací trochu níže.

Obsah tohoto seznamu je naplněn z následujících zdrojů:

  • obsahuje aktuální složku (cwd), případně složku, ve které je uložen spouštěný skript. Toto je i náš případ výše
  • z proměnné prostředí PYTHONPATH
  • předdefinované adresáře s moduly u dané instalace Pythonu

Ukažme si, jak aktuálně sys.path vypadá. Vidíme aktuální složku a pak nějaké další systémové adresáře, do kterých se kouká.

In [17]:
import sys
print(sys.path)
['/home/reitezuz/cv21', '/usr/lib/python311.zip', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', '', '/usr/lib/python3.11/site-packages', '/home/reitezuz/cv21/modules']
In [21]:
# UKAZKA sys.builtin_module_names
import sys
print(sys.builtin_module_names)
print(sys.path)

import os
print(os.environ["PATH"])
print(os.environ.get('PYTHONPATH'))
('_abc', '_ast', '_codecs', '_collections', '_functools', '_imp', '_io', '_locale', '_operator', '_signal', '_sre', '_stat', '_string', '_symtable', '_thread', '_tokenize', '_tracemalloc', '_warnings', '_weakref', 'atexit', 'builtins', 'errno', 'faulthandler', 'gc', 'itertools', 'marshal', 'posix', 'pwd', 'sys', 'time', 'xxsubtype')
['/home/reitezuz/cv21', '/usr/lib/python311.zip', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', '', '/usr/lib/python3.11/site-packages', '/home/reitezuz/cv21/modules']
/usr/local/sbin:/usr/local/bin:/usr/bin
None
Úprava Path¶

Abychom Pythonu řekli, ať hledá moduly ještě v dalších lokacích, můžeme upravit proměnnou prostředí PYTHONPATH a po restartu skriptu už tam začne hledat.

V našem případě (Jupyter notebook) je ale snazší využít další možnost, a to upravit aktuální seznam a přidat tam nějakou dodatečnou složku. Přidáme tedy do seznamu sys.path složku modules sídlící v aktuálním adresáři, abychom pro další práci s moduly mohli využít tuto k tomu určenou složku a nemíchali Jupyter notebooky s Python moduly.

In [3]:
import os
import sys
print(sys.path)
sys.path.append(os.path.join(os.getcwd(), 'modules'))
print(sys.path)
['/home/reitezuz/cv21', '/usr/lib/python311.zip', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', '', '/usr/lib/python3.11/site-packages']
['/home/reitezuz/cv21', '/usr/lib/python311.zip', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', '', '/usr/lib/python3.11/site-packages', '/home/reitezuz/cv21/modules']

Poznámka: Prohlédnutím obsahu proměnné sys.path tedy můžeme zjistit, odkud jsou moduly načítány a naše moduly tam pro opakovatelné použití umístit.

Pro následující kódy již budeme počítat s tím, že moduly můžeme načítat ze složky modules. Obsahuje již nějaké předpřipravené pro další práci.

Spustitelný kód¶

Krom definicí a konstant může modul obsahovat i spustitelný kód. Tento spustitelný kód je spouštěn pouze jednou a to při prvním výskytu daného modulu v příkazu import - tedy při prvním importu modulu, nebo jeho komponent. Tento kód je určený k počátečnímu nastavení a nakonfigurování daného modulu. Taktéž je tento kód spuštěn v případě, že je daný modul (soubor s Python kódem) spuštěn jako skript.

Zkusme znova pustit stejný kód, jako výše. Uvidíme, že podruhé již příkaz import nezavolá funkci print a nic nevypíše:

In [25]:
import mymodule
from mymodule import myvariable
mymodule.say_hello()
Hello! I am function say_hello from module mymodule 

Spouštění jako skript¶

Modul se spustí jako skript tak, že se v terminálu napíše:

python <cesta k modulu> <argumenty>

Poznámka: Jako skript jsme nevědomky spouštěli všechny programy, se který mi jsme doposud pracovali v rámci VSCodium. Kód jsme psali do souboru a ten za nás vývojové prostředí pouštělo pomocí python <nazev_souboru>

In [3]:
# UKAZKA ... v konzoli + zde
import os
os.system('python3 mymodule.py')
This is an executable statement from module __main__
Out[3]:
0

Kód v modulu bude spuštěn stejně, jako při importu, ale navíc se nastaví proměnná __name__ na hodnotu __main__ (string).

Pokud tedy na konec souboru přidáme následující kód, zajistíme, že:

  1. při importu se to bude chovat jako doposud - provedou se nějaké spustitelné kódy, nadefinují se proměnné, třídy a konstanty...jako předtím...
  2. při spuštění jako skript bude nastavena proměnná __name__ na '__main__' a tím pádem bude splněna podmínka a spustí se tento dodatečný kód. Toto je ekvivalent k funkci main, kterou poznáte v jazycích odvozených od C/C++. Tedy vstupní brána do spuštěného programu.
if __name__ == '__main__':
    print('Tato část se spustí jen při zavolání jako skript')
    print('Tady klidně mohu zase zavolat funkci z modulu say_hello')
    say_hello()

Pro ověření funkcionality nejprve zkusme tento upravený modul - druhymodul naimportovat lokálně zde a pak teprve spustit jako skript. Modul se nachází ve složce modules. Podívejte se na jeho obsah.

U importu uvidíme, že importujeme jen jeho název, nemusíme říkat, ve které složce je, to už je díky upravené proměnné path. Dále při prvním importu vidíme vykonání spustitelných kódů.

In [27]:
from druhymodul import myvariable
print(myvariable)
This is an executable statement from module druhymodul
I am variable myvariable from module druhymodul

Pro zavolání jako skript využijeme os.system - spustíme skript nezávisle na tomto Jupyter notebooku. Totéž je možné udělat z terminálu.

In [29]:
import os
os.system('python modules/druhymodul.py')
This is an executable statement from module __main__
Tato část se spustí jen při zavolání jako skript
Tady klidně mohu zase zavolat funkci z modulu say_hello
Hello! I am function say_hello from module __main__ 
Out[29]:
0

Všimněte si, co všechno to vypsalo a že obsah proměnné __name__ je teď __main__.

In [4]:
# ukázka .. funkce main
os.system('python modules/druhymodul_upraveny.py')
This is an executable statement from module __main__
Tato část se spustí jen při zavolání jako skript
Tady klidně mohu zase zavolat funkci say_hello
Hello! I am function say_hello from module __main__ 
Out[4]:
0

Balíčky - packages¶

Co jsou balíčky?¶

Balíček je kolekce modulů organizovaných do adresářové struktury. Může obsahovat také další balíčky. Slouží k lepší organizaci kódu větších projektů a umožňují hierarchickou strukturu modulů. Balíčky jsou reprezentovány adresáři obsahujícími soubory modulů a speciální soubor __init__.py, který indikuje, že daný adresář je považován za balíček.

Vytváření balíčků¶

  • vytvoříme adresář pro balíček (název adresáře odpovídá názvu balíčku)
  • umístíme do něj soubory modulů (.py soubory)
  • vytvoříme v něm soubor __init__.py. Ten způsobí, že daný adresář bude balíčkem. Tento soubor může být prázdný. Může ovšem také obsahovat nějaký inicializační kód, podobně jako u modulů. Takový kód je spuštěn jen jednou při prvotním importu balíčku. Dále může obsahovat proměnnou __all__. O té více později.

Struktura jednoduchého balíčku může vypadat následovně:

nazev_balicku/
├── __init__.py
├── modul1.py
└── modul2.py

Struktura balíčku, který obsahuje i další balíčky. Ukázka dostupná ve složce modules

zpro
├── __init__.py
├── kontejnery
│   ├── __init__.py
│   ├── seznam.py
│   └── slovnik.py
├── obecne.py
└── soubory
    ├── cteni.py
    ├── __init__.py
    └── zapis.py

Importy balíčků¶

Nyní můžeme importovat jednotlivé moduly balíčků. Můžeme používat již známé přístupy:

  • import balicek.modul
  • from balicek import modul
  • from balicek.podbalicek import modul
  • from balicek.podbalicek.modul import funkce

Poznámka: Může se hodit funkce dir, která vypíše dostupný seznam objektů a definic z daného balíčku.

In [8]:
import zpro.kontejnery.seznam
print(dir(zpro.kontejnery.seznam))
print()
# pak můžeme funkcni použít
zpro.kontejnery.seznam.novy_seznam()
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'novy_seznam', 'otoc_seznam']

funkce na novy seznam
In [6]:
from zpro.kontejnery import seznam
seznam.otoc_seznam()

from zpro.kontejnery.seznam import otoc_seznam
otoc_seznam()
funkce na otoceni seznamu
funkce na otoceni seznamu

from import * a proměnná __all__

Můžeme si všimnout, že když naimportujeme vše z balíčku zpro, tak v něm nejsou dostupné všech očekávané podbalíčky a moduly. Oproti očekávání, že tam bude dostupný modul obecne a podbalíčky soubory a kontejnery, je tam pouze podbalíček kontejnery. A i v něm je pouze načtený modul seznam. Tyto tam jsou dostupné pouze protože jsme je dříve nějakým způsobem importovali.

Když zkusíme importovat "všechno", nic se nezmění. Stále tam jsou jen kontejnery.

In [5]:
# pomocna funkce
def filter_keys(keys):
    return [key for key in keys if not key.startswith('_')]
In [6]:
import zpro.kontejnery.seznam
print(filter_keys(dir(zpro)))
print(filter_keys(dir(zpro.kontejnery)))
['kontejnery']
['seznam']
In [7]:
from zpro import *
print(filter_keys(dir(zpro)))
print(filter_keys(dir(zpro.kontejnery)))
print()

print("Globalni ns")
print(filter_keys(dir()))
['kontejnery']
['seznam']

Globalni ns
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'myvariable', 'open', 'os', 'quit', 'sys', 'zpro']

Člověk by očekával, že systém zajistí vyhledání všech podmodulů a importuje je. Ve skutečnosti se tak neděje, protože by to mohla být časově náročná operace a mohla by mít spoustu vedlejších efektů (například nějaká inicializační část submodulu, která by byla potřeba jen v případě, že je submodul explicitně zavolán)

Řešení je, aby autor balíčku poskytl seznam podmodulů, které mají být při takovém importu balíčku-podbalíčku automaticky importovány.

Takový seznam se zapisuje do proměnné __all__, která se umístí vždy do odpovídajícího __init__.py souboru.

Interpret při importu pak importuje všechna vyjmenovaná jména.

Podívejme se na balíček zpro2, který je kopií původního balíčku zpro, jsou upraveny názvy tak, aby končili číslem 2, a obsahuje vyplněné proměnné __all__.

In [8]:
print(filter_keys(dir()))

from zpro2 import *
print(filter_keys(dir()))
obecne2.napis_pisemku2()
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'myvariable', 'open', 'os', 'quit', 'sys', 'zpro']
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'myvariable', 'obecne2', 'open', 'os', 'quit', 'sys', 'zpro']
funkce, co napise pisemku
In [12]:
# toto bude chyba:
from zpro import *
print(filter_keys(dir()))
#zpro.obecne.napis_pisemku() # chyba
obecne.napis_pisemku() # chyba
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'open', 'os', 'otoc_seznam', 'quit', 'seznam', 'sys', 'zpro']
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 5
      3 print(filter_keys(dir()))
      4 #zpro.obecne.napis_pisemku()
----> 5 obecne.napis_pisemku()

NameError: name 'obecne' is not defined

Můžeme vidět již automaticky naimportovaný modul obecne2

Obdobně při importu balíčku zpro2.kontejnery2, dostaneme moduly jak seznam2 tak slovnik2

In [35]:
from zpro2.kontejnery2 import *
print(filter_keys(dir()))
['In', 'Out', 'debugpy', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'kte_prvocislo', 'matematika', 'mymodule', 'myvariable', 'obecne2', 'open', 'os', 'otoc_seznam', 'quit', 'seznam', 'seznam2', 'slovnik2', 'sys', 'vypis_prvocisla', 'vypiš_dělitele', 'zpro']

__all__ na úrovni modulu¶

Podobně jako u balíčků proměnná __all__ umí řídit i to, co se importuje při importu "všeho" z modulu. Stačí ji umístit do samotného modulu.

Pokud se v modulu __all__ nachází, importují se jen objekty, které jsou vyjmenované. Pokud se tam nenachází, importují se všechna jména, která nezačínají podtržítkem (_)

Podívejte na obsah souboru seznam2.py

Pozorujte, že z modulu seznam2 se mi importovala jen funkce novy_seznam2, protože její jméno je zapsáno v __all__. Druhá funkce otoc_seznam2 přítomna není.

Poznámka: můžeme importovat i ty objekty, které nejsou vyjmenovány v proměnné __all__. Musíme ale explicitně vypsat jejich název, například from zpro2.kontejnery2.seznam2 import otoc_seznam2

In [13]:
from zpro2.kontejnery2.seznam2 import *
print(filter_keys(dir()))

from zpro2.kontejnery2.seznam2 import otoc_seznam2
otoc_seznam2()
print(filter_keys(dir()))

from zpro2.kontejnery2  import seznam2
seznam2.otoc_seznam2()
print(filter_keys(dir()))
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'novy_seznam2', 'open', 'os', 'otoc_seznam', 'quit', 'seznam', 'sys', 'zpro']
funkce na otoceni seznamu
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'novy_seznam2', 'open', 'os', 'otoc_seznam', 'otoc_seznam2', 'quit', 'seznam', 'sys', 'zpro']
funkce na otoceni seznamu
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kontejnery', 'novy_seznam2', 'open', 'os', 'otoc_seznam', 'otoc_seznam2', 'quit', 'seznam', 'seznam2', 'sys', 'zpro']

Relativní import¶

Pokud potřebujeme v rámci balíčků importovat subbalíčky/submoduly/funkce z tohoto balíčku, můžeme krom absolutních importu použít i importy relativní. Relativní importy operují v rámci prostoru daného balíčku.

Relativní import používá předponu . pro značení aktuálního (sub)balíčku nebo .. pro specifikaci nadřazeného (rodičovského) balíčku.

Příklad naleznete z souboru: seznam2.py

from . import slovnik2  # import z vedlejsiho modulu v tomto balicku
from ..soubory2 import zapis2  # import z vedlejsiho subbalicku
from ..obecne2  import napis_pisemku2   # import z nadazeneho balicku
In [40]:
import zpro2.kontejnery2.seznam2
zpro2.kontejnery2.seznam2.pouzij_relativni_import()
funkce na novy slovnik
funkce na zapis textovych souboru
funkce, co napise pisemku

Příklady¶

1) dokončete látku a příklady z minulé hodiny¶

In [ ]:
# dělali jsme příklady na argparse z minule

2) vytvořte modul s matematickými funkcemi¶

V dřívějších cvičeních jsme často vytvářeli funkce, které počítají například faktoriál, zjišťují, jestli je číslo sudé nebo liché, počítají prvočíselný rozklad a tak podobně.

Sesbírejte tyto funkce a udělejte z nich modul matematika.

poznámka: využijte možnosti definovat __all__ a vyjmenujte tam jen užitečné funkce - pomocné funkce nechť tam nejsou. Ověřte funkcionalitu pomoci from matematika import *

poznámka: Může být snadnější v tomto ohledu pracovat ve VSCodium nebo v terminálu, než na Jupyter Hubu.

In [4]:
print(filter_keys(dir()))

from matematika import *

print(filter_keys(dir()))
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'open', 'os', 'quit', 'sys']
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kte_prvocislo', 'open', 'os', 'quit', 'sys', 'vypis_prvocisla', 'vypiš_dělitele']
In [5]:
vypiš_dělitele(70)
1 2 5 7 10 14 35 70 
In [10]:
os.system('python modules/matematika.py')
10-té prvočíslo je 29
Out[10]:
0

3) vytvořte modul s funkcemi pro práci se soubory¶

V dřívějších cvičeních jsme často vytvářeli funkce, které pracují se soubory.

Sesbírejte tyto funkce a udělejte z nich modul soubory.

poznámka: Může být snadnější v tomto ohledu pracovat ve VSCodium nebo v terminálu, než na Jupyter Hubu.

4) Vytvoření balíčku¶

Vytvořte nový balíček nazvaný vsehochut. Do něj zakomponujte výše vytvořené moduly. Dále zkuste experimentovat s vytvářením podbalíčků. Podle libosti doplňte další funkcionalitu

poznámka: Může být snadnější v tomto ohledu pracovat ve VSCodium nebo v terminálu, než na Jupyter Hubu.

In [5]:
print(filter_keys(dir()))

from balicek_pokus.matematika import *

print(filter_keys(dir()))
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'open', 'os', 'quit', 'sys']
['In', 'Out', 'exit', 'filter_keys', 'get_ipython', 'kte_prvocislo', 'open', 'os', 'quit', 'sys', 'vypis_prvocisla', 'vypiš_dělitele']