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__.
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:
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
# 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:
- podívá se, jestli modul není součástí vestavěných modulů (jejich seznam je dostupný v rámci
sys.builtin_module_names) - Pokud ho nenalezne mezi vestavěnými moduly, hledá soubor
mymodule.pyv seznamu adresářů uloženém v proměnnésys.path
Poznámka: V těchto cestách krom souboru
mymodule.pyhledá i složku nazvanoumymoduleobsahují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á.
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']
# 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.
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.pathtedy 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:
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>
# UKAZKA ... v konzoli + zde
import os
os.system('python3 mymodule.py')
This is an executable statement from module __main__
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:
- 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...
- 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 funkcimain, kterou poznáte v jazycích odvozených odC/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ů.
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.
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__
0
Všimněte si, co všechno to vypsalo a že obsah proměnné __name__ je teď __main__.
# 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__
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ů (
.pysoubory) - 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.modulfrom balicek import modulfrom balicek.podbalicek import modulfrom 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.
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
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__
__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.
# pomocna funkce
def filter_keys(keys):
return [key for key in keys if not key.startswith('_')]
import zpro.kontejnery.seznam
print(filter_keys(dir(zpro)))
print(filter_keys(dir(zpro.kontejnery)))
['kontejnery'] ['seznam']
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__.
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
# 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
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říkladfrom zpro2.kontejnery2.seznam2 import otoc_seznam2
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
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¶
# 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 pomocifrom matematika import *
poznámka: Může být snadnější v tomto ohledu pracovat ve VSCodium nebo v terminálu, než na Jupyter Hubu.
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']
vypiš_dělitele(70)
1 2 5 7 10 14 35 70
os.system('python modules/matematika.py')
10-té prvočíslo je 29
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.
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']