Proč Python svádí k psaní dlouhých souborů
Python a jeho způsob práce s namespacy dle mého mínění svádí k psaní dlouhých souborů. Rád bych zde ukázal proč a jak se situace liší v PHP.
Python je specifický tím, že adresáře a soubory vytvářejí jmenné prostory (balíčky a moduly). Tato vlastnost je na jednu stranu hodně příjemná - není potřeba explicitně ve zdrojáku definovat namespace, ale na druhou stranu zesložiťuje rozdělení tříd jednoho namespacu do samostatných souborů.
Příklad
Mějme třídy Post a PostRepository. Post je entita (např. Doctrine2 v PHP a SQLAlchemy v Pythonu), PostRepository má na starost vybírání článků z databáze - podle ID, podle autora, apod. Obě třídy jsou na počátku projektu malé, řekněme 50 řádek kódu.
PHP
V PHP je dnes již běžná praxe každou třídu umístit do samostatného souboru (snad kromě vyjímek, které bývají jednořádkové). Budeme tedy mít adresář Post a v něm dva soubory Post.php a PostRepository.php. Oba soubory budou v namespace Post. Použití tříd pak tedy bude vypadat nějak takto:
use PostPost; use PostPostRepository; $post = new Post(); $post->setName('..'); $repo = new PostRepository(); $posts = $repo->findAllByAuthor(...);
Python
V Pythonu je běžné zapsat více krátkých tříd do jednoho modulu. Od toho ostatně moduly jsou, aby seskupovaly třídy a funkce do znovupoužitelných jednotek. Budeme tedy mít soubor post.py a v něm obě třídy. Použití pak bude vypadat nějak takto:
from post import Post, PostRepository post = Post() post.name = '...' repo = PostRepository() posts = repo.findAllByAuthor(...)
Zatím tedy všechno vypadá v pořádku, když nám ale obě třídy časem nabobtnají, stane se práce se souborem post.py dost nepříjemnou. Mohli bychom soubor rozdělit a umístit třídu PostRepository do samostatného souboru post_repository.py, ale to by znamenalo upravit všechny importy, kde se třída používá, což není zrovna příjemné a v případě knihoven silně nežádoucí (BC break).
Naštěstí máme v Pythonu ještě jinou možnost, jak tuto situaci vyřešit a to je vytvoření balíku (složky) post a import tříd v __init__.py. Budeme tedy mít složku post se soubory post.py, post_repository.py a __init__.py, který bude obsahovat:
from .post import Post from .post_repository import PostRepository
Takovéhle rozdělení není vždy úplně triviální, musíme rozdělit všechny importy a moduly často obsahují také řadu funkcí.
Důkaz místo slov
Abych svojí domněnku podpořil, porovnal jsem délku souborů v několika podobných projektech: Doctrine2 vs SQLAlchemy, Nette vs Flask a Symfony vs Django.
Průměrnou délku souborů spočítáme třeba takto:
find ./sqlalchemy/lib -name '*.py' -not -name __init__.py -print0 | wc -l --files0-from=- | head -n -1 | cut -d" " -f1 | awk '{sum+=$1} END { print "Average = ",sum/NR}'
Projekt | Průměrná délka souboru | Nejdelší soubor |
---|---|---|
SQLAlechemy | 611.799 | 3893 |
Doctrine2 | 183.537 | 3394 |
Flask | 325.947 | 1907 |
Nette | 193.562 | 1450 |
Django | 170.09 | 2435 |
Symfony | 109.44 | 1973 |