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.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 nazvanoumymodule
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á.
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.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:
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ů (
.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.
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']