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