for i in zipfile:
Nedávno jsem řešil problém procházení adresářové struktury a nalezení a zpracování určitého typu souborů. S drobnou komplikací: Některé soubory mohou být zip archívy, jiné soubory mohou být komprimované jako gzip. Nu a vzpoměl jsem si na starší přednášku Brandona Rhodese The Clean Architecture in Python (video, slides).
Burying I/O instead of decoupling it
Brandon krásně popisuje jednu běžnou architektonickou chybu, kterou
my programátoři děláme. Vezměme si jako příklad úkol: Udělat nějakou
akci se všemi soubory s příponou .txt
v adresáři. Mohlo by
to vypadat třeba takto:
for i in os.listdir(dir):
if i.endswith(".txt"):
perform_operation(os.path.join(dir, i))
Ano, ten vstup nebo výstup jsme tady zakopali někam dovnitř aplikace, ikdyž popravdě namítnete, že celý kód je tak krátký, až je přehledný. Co když budeme chtít něco víc? Například jít do podadresářů?
def process_directory(dir):
for i in os.listdir(dir):
full_fn = os.path.join(dir, i)
if os.path.isdir(full_fn):
process_directory(full_fn)
elif i.endswith(".txt"):
perform_operation(full_fn)
Zakopanější a rekurzivní. Standardní knihovna pythonu nám
tady nabízí konstrukci os.walk
. S její pomocí se zbavíme
rekurze (přesněji řečeno os.walk
ji udělá za nás).
K dokonalosti ale kousek chybí:
for (root, dirs, files) in os.walk(dir):
for i in files:
if i.endswith(".txt"):
perform_operation(os.path.join(dir, root, i))
Pořád tu máme blok, který dělá I/O, a uvnitř v něm je provedení té vlastní funkcionality. Tak ještě jeden pokus:
def select_files_of_interest(dir):
for (root, dirs, files) in os.walk(dir):
if i.endswith(".txt"):
yield os.path.join(dir, root, i)
for full_fn in select_files_of_interest(dir):
perform_operation(full_fn)
Tady už jsme úplně oddělili to procházení adresářů - získání souborů, které nás zajímají - od vlastního zpracovnání. A totéž teď uděláme pro zip.
A teď dovnitř zipu
Napíšeme funkci deep_walk
, která dostane vstupní
bod do souborového systému (adresář, soubor, file-like objekt),
bude přes ni iterovat, a tím se dostane k jednotlivým souborům,
ať už přímo ze souborového systému, nebo ze zip souborů. Vlastní
funkce je jednoduchá. Než abychom si ji nějak dopředu rozebírali
se na ni prostě podíváme:
from io import BytesIO
import os
import os.path
import sys
import zipfile
# for python 2, instead of from io import BytesIO:
# from six import BytesIO
def deep_walk(f, fn=None):
"""
Walks through all files within the fn. Visits files in all subdirectories,
extracts zip files and processes all files with the zip files the same
way (that means, if there is a zip file inside a zip file, it is extracted
and processed as well.
:param f: The starting file or directory or a seekable file-like object
open for reading.
:param fn: The file name
:returns: Yields (filename, file_object) for each file within f. Walks
into directories, extracts zip files on the way.
"""
if hasattr(f, "read"): # a file-like object
if zipfile.is_zipfile(f):
zf = zipfile.ZipFile(f, mode="r", allowZip64=True)
for i in zf.infolist():
with zf.open(i) as sub_f:
for ii in deep_walk(BytesIO(sub_f.read()),
"%s:%s" % (fn, i.filename)):
yield ii
else: # just return an already opened file
yield (fn, f)
elif os.path.isdir(f):
for (root, dirs, files) in os.walk(f):
for i in files:
full_path = os.path.join(root, i)
with open(full_path, "rb") as sub_f:
for ii in deep_walk(sub_f, "%s:%s" % (fn, i)):
yield ii
elif zipfile.is_zipfile(f):
with open(f, "rb") as zf:
for ii in deep_walk(zf, f):
yield ii
else:
raise ValueError("The parameter f must be a zipfile or a directory.")
Při použití máme krásně oddělený vstup/výstup od vlastní funkcionality:
for (fn, f_obj) in deep_walk(directory_with_zip_files):
if fn.endswith(".txt"):
perform_operation(f_obj)
Poznámky:
- Tohle je generátor.
- Všimněte si, že soubor otevřený ze zipu obalím do
BytesIO
, než ho rekurzivně zpracovávám. Je to proto, žeZipFile
potřebuje seekable stream, neboli objekt, který implementuje funkciseek
, což soubor otevřený ze zipu není. Což ale znamená, že se každý takto zpracovávaný soubor načte do paměti! - Ještě jednou: Všechno běží v paměti, takže počítáme jen se soubory tak malými, že se do paměti vejdou, a to v nejhorším případě i všechny najednou.
- Neřešíme výjimky. Zde záleží na aplikaci, ideální by asi bylo řešení
jako má
os.walk
- argumentonerror
. - V souborovém příkladu generátoru jsem vložil ten test na
*.txt
dovnitř generátoru. Zde nechávám funkci obecnou - vrátí všechny soubory. Můžeme uvážit napsání druhého generátoru, který obalídeep_walk
, a bude jen dělat kontrolu na příponu souboru. Ale nahrazovat jednořádkovou podmínku generátorem mi naopak přijde jako neopodstatněné zesložitění. - Mohli bychom přidat podporu gzipu a dalších formátů.
Slovo o výjimkách
Schválně si zkuste, jak se bude program chovat, když někde vznikne výjimka. Uděláme si takový umělý případ:
def exception_test_generator():
for i in range(10):
try:
yield i
except Exception: # catch-all
print("Caught an exception in the iterator")
for i in exception_test_generator():
raise Exception()
Tipněte si, jak to dopadne. Zachytí výjimku ten try...except
ve funkci
exception_test_generator
? A je to tak správně? Nebo jej nezachytí?
A je to tak správně?
Protože nejcennější znalosti jsou ty, které získáte vlastní prací, tak
Vám tentokrát odpověď nedám.
Závěr
Když jste začali číst, pravděpodobně Vás napadlo slovo callback. A taky že ano. Callback je další a pravděpodobně starší způsob, jak podobné problémy řešit. Ale zkuste porovnat čitelnost:
# callback:
deep_walk(directory_with_zip_files, callback=process_file)
# generator:
for (fn, f_obj) in deep_walk(directory_with_zip_files):
process_file(f_obj)
Verze s generátorem rozhodně není kratší, ale pro mě je čitelnější, protože nezakrývá, ale naopak zvýrazňuje tu logiku a strukturu. Kromě toho umožňuje snažší zpracování výjimek. Stejně, jako chceme oddělit vstup a výstup od vlastní funkcionality, chceme od vlastní funkcionality oddělit i zpracování chybových / výjimečných stavů.
Na úplný závěr zopakuju odkaz na přednášku Brandona Rhodese The Clean Architecture in Python (video, slides) a přidám ještě jeden: Raymond Hettinger - Beyond PEP 8 -- Best practices for beautiful intelligible code - PyCon 2015.
Komentáře byly zrušeny
V EU teď máme složitou situaci s Cookies. Na komentáře jsem používal jistou službu třetí strany. Ta však používá Cookies poměrně, ehm, benevolentně. Tak jsem se rozhodl komentáře zrušit. Pokud chcete, můžete mi napsat přímo