setup.py nebo requirements.txt?

Pár dní zpět Armin Ronacher (tvůrce Flasku, Werkzeugu, Jinjy a dalších) tweetnul, ať se přestane používat requirements.txt a použije se setup.py s přepínačem --editable. Což rozpoutalo menší diskuzi na Twitteru i u nás v práci. U obou diskuzí se odkazovalo také na starší článek popisující rozdíl mezi těmito možnostnostmi.

A v čem je tedy ten setup.py lepší, resp. proč ho Armin doporučuje oproti requirements.txt?

Jak už je zmíněno v článku, setup.py má na starost seznam abstraktních závislostí. Dá se to přirovnat jako název tagu bez konkrétního Git repozitáře. Prostě chci requests ve verzi 2.10, ale neřeším, odkud se závislost stáhne. Může to být oficiální pypi.org, ale taky odkudkoliv jinud. Jenže to nestačí, pokud se v requests objeví chyba, kterou si opravím ve vlastním forku a nemohu čekat, než se dostane do upstreamu. Pak tu je requirements.txt, kam lze napsat odkud se má závislost stáhnout, klidně na konkrétní commit.

Jak tedy řešit takové případy bez requirements.txt?

Než odpovím, je potřeba si uvědomit složitější závislosti. Mám aplikaci a ta využívá například requests a knihovnu na stahování a parsování XML, která taky využívá requests. V případě vlastní verze requests bych pak musel nejen upravit závislost v mé aplikaci, ale také v knihovně, která vyžaduje requests. Což by vedlo k dalšímu forku jen kvůli jiné závislosti. Samozřejmě čím zanořenější závislosti budou, tím se stane komplikovanější něco takového udržovat.

Proto je lepší zůstat u abstraktních závislostí a při instalaci jen říkat, odkud se mají závislosti stahovat. Mohu buď využívat oficiální pypi nebo si udělat klidně i vlastní server. Při problému pak dám do svého registru balíků verzi, kterou potřebuji, a nemusím měnit všude závislosti na konkrétní.

Dobře, to mi dává smysl. Ale je to vhodné pro všechny?

Není. Pro střední a větší firmy určitě, pro malý soukromý projekt spíš ne. Navíc osobně mi přijde nejlepší aplikaci balit do prostředí, kde má běžet. Pokud je to Debian, pak nejlepší cesta je debianí balíček. S ním mohu řešit nejen Pythoní závislosti, ale také Céčkové a jiné závislosti a spoustu dalších věcí jako konfigurační soubory, logrotate, cron, … Vše hezky v jednom.

Ale princip zůstává stejný. Pořád je potřeba řešit vlastní verze, ať už z důvodu, že jsem opravil chybu, která ještě není mergnutá a publikovaná v upstreamu, či daná knihovna není ubalena jako deb balík. Opět nejlehčí mít vlastní debianí strom balíků.

No a co ten přepínač --editable?

Znáte takovou situaci, kdy upravíte knihovnu, nainstalujete lokálně, jdete vyzkoušet, zda aplikace si s ní rozumí, nerozumí, a tak proces opakujete? Tak právě --editable zařídí, že se nainstaluje jednou a změny jsou ihned dostupné. Soubory se nezkopírují, ale nalinkují se do pracovního adresáře. Tedy lehčí debugging.

Abych to shrnul, se setup.py je jednodušší správa závislostí, lehčí debugování a mimochodem to také zařídí, že vaše aplikace je importovatelná. Což je o čem Armin tweetoval. No a třeba své důvody brzy sám ještě upřesní. :-)

Python 3.0 až 3.5

Python 2.7 se hodně používá a zřejmě nějaký ten pátek ještě bude. Přechod na Python 3 se ale nevyhnutelně blíží a při takovém přechodu se skočí rovnou na nějakou nejposlednější verzi. Momentálně to může být 3.4 či dokonce před pár dny vypuštěný Python 3.5. Říkal jsem si, co vlastně všechno takového hezkého bylo přidáno od verze 3.0 do 3.5 a nebylo backportováno? Prošel jsem si všechny změny a řekl si, že se podělím o to, co mne zaujalo.

Python 3.0: Print!

print() je funkce. S tím bude těžké sžít se. První třetí verze se objevila v roce 2009 a stále jsem se nenaučil psát závorky.

Další velká změna je, že text je opravdu text a nejde mixovat s binárními daty. Takže žádné fungování při testech a poté v provozu neustálé ošetřování výjimek UnicodeDecodeError.

Všude jsou iterátory. Žádné range vs. xrange, ale pouze range vracející iterátor. Další funkce dříve vracející seznamy také jedou na iterátorech, jako třeba map, filter, zip, … Ale pozor, items, keys apod. nad slovníkem nevrací iterátor, nýbrž view. To je zase trochu něco jiného.

super() bez parametrů!

A detail možnost rozbalit tuple s hvězdičkou: (a, *rest, b) = range(5).

Python 3.1: Vůbec nic

Vše důležité bylo backportnuto i do Pythonu 2.7. Nový Python si holt teprve sedal. :-)

Python 3.2: První featury

První krůčky k asynchronnímu Pythonu, knihovna concurrent.futures.

Konečně si nebudeme muset dělat sami řešení na cachování výsledků funkcí, aneb ať žije lru_cache!

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Ve functools přibyla ještě jedna zajímavost a to, že wraps navíc přidává __wrapped__ odkazující na původní funkci. Takže jde introspektnout, vrátit atp. :-)

OrderedDict dostal novou metodu move_to_end. Jen škoda, že opak udělali ošklivě parametrem…

Je možné mít configuráky s proměnnými díky ExtendedInterpolation. Mimochodem lze předat konfiguraci i jako slovník, což dodává proměnným ještě větší šťávu!

>>> parser = ConfigParser(interpolation=ExtendedInterpolation())
>>> parser.read_dict({'buildout': {'directory': '/home/ambv/zope9'}})
>>> parser.read_string("""
    [buildout]
    find-links = ${buildout:directory}/downloads/dist
    """)
>>> parser['buildout']['find-links']
'/home/ambv/zope9/downloads/dist'

Vzniknul modul argparse. Ten byl teda backportnut i do Pythonu 2.7. Ale chci se pochlubit, že existuje i argparsedialog (jen pro trojku).

Python 3.3: Rozjíždíme…

Je to tu. Tak kolik aplikací 3.3ka složí? :-) Slovník nemá definované pořadí. V dokumentaci bylo upozornění, že se nejedná o náhodné řazení, ale rozhodně se na to nemá spoléhat. Kvůli bezpečnostní chybě už nelze pořadí předvídat.

Nová syntaxe yield from pro delegování generatorů. Především pro to, co přijde v Pythonu 3.4, aby se později stalo v 2.5 zbytečným. :-)) Zkratka pro:

>>> for item in iterable:
...    yield item

>>> yield from iterable

Virtualenv už není třeba, součástí Pythonu je nový pyvenv, který řeší všechny problémy, které nemohly tooly třetích stran vyřešit na 100 procent.

__init__.py není povinný! Ano, NE-NÍ povinný. Jen otázka, zda je to dobrý nápad. Protože pak je možné naimportovat jakýkoliv adresář…

Pokud se odchytí výjimka a ta vyhodí novou výjimku, budou vidět obě. Často zbytečné, ale věřte, že v praxi se stávají i chyby při zpracování chyby, a pak je akorát původní chyba občas nedohledatelná.

>>> class C:
...     def __init__(self, extra):
...         self._extra_attributes = extra
...     def __getattr__(self, attr):
...         try:
...             return self._extra_attributes[attr]
...         except KeyError:
...             raise AttributeError(attr)
...
>>> C({}).x
Traceback (most recent call last):
  File "", line 6, in __getattr__
KeyError: 'x'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "", line 1, in 
  File "", line 8, in __getattr__
AttributeError: x

Přes atribut __qualname__ půjde nově zjistit přesně, kde byla funkce či třída definována.

>>> class C:
...     def meth(self):
...         pass
>>> C.meth.__name__
'meth'
>>> C.meth.__qualname__
'C.meth'

Vytváření souborů a vyhodit výjimku, pokud už existuje, půjde s novým módem x.

>>> open('/tmp/somefile', 'x')
<_io.TextIOWrapper name='/tmp/aaa' mode='x' encoding='ANSI_X3.4-1968'>
>>> open('/tmp/somefile', 'x')
Traceback (most recent call last):
  File "", line 1, in 
FileExistsError: [Errno 17] File exists: '/tmp/somefile'

datetime instance dostala novou metodu timestamp. Ale že to trvalo, že? :-)

>>> some_datetime.timestamp()
1442266005.577331

Nový hezký Pythonic modul ipaddress. Díky tomu lze zapsat například: (IPv4Address('127.0.0.1') + 42).is_private.

mock je součástí unittestu!

Je zpět u'' pro lepší port z dvojky! Není to jediná drobnost, která ulehčuje přechod na trojku. Takže pokud přechod, tak minimálně na… 3.4 (ne, to není překlep, jen dnes dřívější nemá moc smysl).

Python 3.4: Balíky, balíky, balíky, …

ensurepip je nový balík zajišťující přítomnost instalací Pythonu, i když se jedná o projekt mimo samotný Python. Jinými slovy pip po ruce po instalaci Pythonu!

Nový balík pro asynchronní aplikace AsyncIO. To je kde se využije delegování generátorů. AsyncIO je totiž na generátorech tak trochu postavené.

Vlastně až balík pathlib mne nahlodal projít podrobně, co vše v Pythonu 3.x bylo přidáno a upraveno.

>>> p = Path('/etc')
>>> q = p / 'init.d' / 'reboot'
>>> q
PosixPath('/etc/init.d/reboot')
>>> q.resolve()
PosixPath('/etc/rc.d/init.d/halt')

Třeba takový enum se bude taky hodit…

>>> from enum import Enum
>>> class Color(Enum):
...     red = 1
...     green = 2
...     blue = 3
>>> type(Color.red)
<enum 'Color'>
>>> isinstance(Color.green, Color)
True
>>> print(Color.red.name)
red

Nebo různé statistické metody v modulu statistics

Modul functools se rozrostl o další užitečnosti. První z nich je partialmethod, což dodává super nové možnosti:

>>> class Cell(object):
...     def __init__(self):
...         self._alive = False
...     @property
...     def alive(self):
...         return self._alive
...     def set_state(self, state):
...         self._alive = bool(state)
...     set_alive = partialmethod(set_state, True)
...     set_dead = partialmethod(set_state, False)

A pokud vám chybělo method overloading, pak to lze aspoň trochu dohnat se singledispatch. I když nedoporučoval bych takový přístup.

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)
...
>>> @fun.register(int)
... def _(arg, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42

Občas se může stát, že se do reguláru zapomene přidat dolar nebo se při nějakých změnách vytratí atp., proto další metoda fullmatch.

Zrovna nedávno jsme řešili, že naše aplikace žere docela dost paměti a občas neúměrně moc. Chtěli jsme to pořádně zanalyzovat, ale nebylo čím. Mile mne překvapil modul tracemalloc. S ním to bude sranda debugovat. :-)

Funkce min a max požírají parametr default, řešící současné vyhazování ValueError při prázdné iterable proměnné.

Python 3.5: To nejlepší nakonec

Už žádné yield from s AsyncIO. Vznik nových klíčových slov async a await. Tedy abych byl přesný, pravými klíčovými slovy budou až od Pythonu 3.7, ale je dobré se jim vyhnout už nyní.

async def read_data(db):
    data = await db.fetch('SELECT ...')

Type hinty. Druhá velká věc, o které se hodně mluví. Za mne se moc těším na možnost sem tam vynutit typ a pomoct tak i editorům v napovídání. Vlastně nemusím, jelikož se stub fily lze využít už nyní. Ale přímo v kódu je přímo v kódu. Konečně žádné cusotm dekorátory či asserty na začátku funkce.

def greeting(name: str) -> str:
    return 'Hello ' + name

Jen se mi moc nelíbí jak se budou zapisovat nějaké složitější typy s dodávanou knihovnou typing. No, diskutovalo kolem toho spoustu chytrých lidí, tak uvidíme, jaké to bude v praxi.

Krom nových klíčových slov a type hintů se dostal do Pythonu i nový operýtor @ pro násobení matic. Známá knihovna numpy přijde s podporou ve verzi 1.10. Matematici se mohou na co těšit. :-)

>>> x = numpy.ones(3)
>>> x
array([ 1., 1., 1.])

>>> m = numpy.eye(3)
>>> m
array([[ 1., 0., 0.],
       [ 0., 1., 0.],
       [ 0., 0., 1.]])

>>> x @ m
array([ 1., 1., 1.])

Kolikrát jste potřebovali zmergovat slovník a používali pro to ne úplně jasnou konstrukci dict(first, **second)? Tak nově lze pěkně hezky: {**first, **second}. Samozřejmostí je funkčnost i se seznamy, sety apod.

A to víte, že existuje možnost zabalit Pythoní aplikaci do zipu a přímo spustit? Se zipapp už ano. Zajímavá metoda pro drobnosti, které nepatří do PyPI a je zbytečné dělat jiný, třeba debianí, balík.

configparser ještě jednou. Je možné s předáním converters přidat další get* metody.

>>> parser = ConfigParser(converters={'list': [item.strip() for item in value.split() if item.strip()]})    
>>> parser.read_string("""[section]
... value = a b c d""")
>>> parser.get('section', 'value')
'a b c d'
>>> parser.getlist('section', 'value')
['a', 'b', 'c', 'd']

Pár zajímavých věcí bylo přepsáno do céčka. Například OrderedDict či lru_cache. A spoustu dalších optimalizací přes všechny releasy, jako třeba zrychlení načtení interpretu, rychlejší dumpování všeho druhu (pickle, marshall, json), rychlejší volání get property, optimalizace podmínky s výčtem (tedy lepší psát if x in {a, b, c}), …

Mne nejvíce potěšilo…

  • Vždy přítomný pip a pyvenv
  • Možnost občas hintit typy
  • Asynchronní aplikace s AsyncIO
  • Jednoduše mergovat slovníky {**first, **second}
  • Rozbalování tuplů s hvězdičkou (a, *rest, b) = range(5)
  • Možnost předat funkcím min a max defaultní hodnotu
  • Všude iterátory
  • Cachování s lru_cache
  • Práce s cestami s pathlib

Takže… taky byste nejradši hned zahodili současný codebase a použili Python 3.5? :-)

Django vs. Flask, Rails vs. Sinatra

Výběr frameworku je vždy náročný úkol. V Pythonu jsou nejznámější webové frameworky Django a Flask, který ale vybrat? Vyzkoušeny mám oba pro velké projekty za různých okolností a nemám výherce. Každý je dobrý k něčemu jinému. V Ruby je podobná volba Rails nebo Sinatra. To, že nemám výherce, ale neznamená, že nevím, po kterém sáhnout. Naopak dnes už to vím přesně. Popíšu na dvou případech:

1) Když jsem začínal BOObook.cz, neměl jsem nic a potřeboval jsem rychle něco mít. Sáhl jsem po Djangu, připravil projekt, a ještě tu noc jsem měl základní kostru se šablonami, formuláři, modelem, databází, sessionou a administrací. Mohl jsem se zaměřit na featury a jakmile něco nevyhovovalo, postupně měnit za svůj kód. Například vyměnit/upravit administraci, vlastní servírování souborů chráněné přihlášením a podobně.

2) Když jsme si v práci řekli, že je na čase přejít na open source framework (z našeho interního z dob, kdy ještě nic takového neexistovalo), sáhli jsme po Flasku. Měli jsme totiž vše a Flask nemá nic. Díky tomu nám dovolil ho použít ihned a postupně přecházet na nové šablony, formuláře, modely, session atp. Byl to sice komplikovanější proces, než začít paralelně na zelené louce, jenže přepsat celou aplikaci by trvalo roky. Nyní máme přechod za sebou a funguje nám jak nová, tak stará, ještě nepřepsaná, část aplikace.

Co to tedy znamená?

Django je all-in-one balíček. Od routingu, přes modely a formuláře až po šablony. Samozřejmě nechybí spoustu šikovných či dokonce potřebných utilit. Tedy nainstalujete, přečtete si dokumentaci, první ukázku a můžete se zaměřit na vaši aplikaci.

Flask je naopak vlastně jen routing a vše se musí poskládat z dalších knihoven dle potřeby. Často Jinja, WTForms, SQLAlchemy a další. Tedy nainstalujete, přečtete si dokumentaci a hledáte best practices, jak komponenty poskládat dohromady pro vaši potřebu.

Z toho vyplývá mé doporučení: pokud nemusíte použít nějaké vlastní komponenty, je lepší sáhnout po Djangu. Díky tomu se lze zaměřit na vývoj webové aplikace a neřešit, jak validovat vstupní data, jak si držet sessionu, jak přistupovat k databázi a dalším zdrojům, … S jednou výjimkou – Django je kanón na vrabce, takže nemá smysl se ho držet pro opravdu miniaturní webovou stránku.

Co Rails vs. Sinatra?

Tam to vidím stejně. S tím, že Django se jmenuje Rails a Flask je Sinatra.

Jediný rozdíl vidím ve výjimce. Chtěl jsem se podívat, co zajímavého nabízí Ruby, a tak ho využil pro jedno API k mému novému pet projektu. Jelikož se jedná o SPA a veškerý kód je v JavaScriptu, sáhl jsem po Sinatře. Což byla asi chyba, jelikož jako ve světe JavaScriptu často lidi radí, jak problém vyřešit s jQuery, ve světe Ruby naopak radí, jak problém vyřešit s Rails. Je pak těžké najít čisté Ruby. Na druhou stranu s Rails bych nevěděl, co znamená čisté Ruby. :-)

Co se mi líbí na pytestu

Stručně jsem tu už o pytestu psal v poznámkách z konference PyCON UK. Od té doby jsme v našem týmu pytest nasadili na několik komponent a testuje se nám parádně. Proč?

Asserty

Není potřeba psát více, než je potřeba. Python už assert v sobě má.

def test_eq():
    assert 1 == 2

Spustíme…

$ py.test test.py
=========================== test session starts ===========================
platform linux2 -- Python 2.7.3 -- py-1.4.25 -- pytest-2.6.3
collected 1 items

test.py F

================================= FAILURES ================================
--------------------------------- test_eq ---------------------------------

    def test_eq():
>   	assert 1 == 2
E    assert 1 == 2

test.py:6: AssertionError
========================= 1 failed in 0.01 seconds =======================

Super! Tímto samozřejmě pytest nekončí. Například balík unittest dodává při assertu různé pomocné hlášky. Tady začíná trochu magie. Python sám o sobě nic víc při použití assertu neřekne, proto pytest udělá menší analýzu kódu a vyplivne podrobný rozbor, třeba i s diffem.

def test_eq():
    a = 'aa'
    assert a == 'aab'
    def test_eq():
    	a = 'aa'
>   	assert a == 'aab'
E    assert 'aa' == 'aab'
E      - aa
E      + aab
E      ?   +

Poradí si i s instancemi a vnořenými instrukcemi. Co ale další typy, třeba assertIn, assertIsInstance, …?

    def test_in():
>   	assert 42 in range(10)
E    assert 42 in [0, 1, 2, 3, 4, 5, ...]
E     +  where [0, 1, 2, 3, 4, 5, ...] = range(10)
    def test_isinstance():
>   	assert isinstance(datetime.date.today(), datetime.datetime)
E    assert isinstance(datetime.date(2015, 2, 14), )
E     +  where datetime.date(2015, 2, 14) = ()
E     +    where  = .today
E     +      where  = datetime.date
E     +  and    = datetime.datetime

Tady je vidět, že občas pytest trochu spamuje. V tomto případě je nám jedno, jakou konkrétní hodnotu datum obsahuje a pokud by se hodilo, stačí vědět datum, nikoliv kde všude v paměti se vše nachází. Ono je to ale takhle lepší, jak zjistíme později. Hlavně pokud se to děje samo s kódem, který musíme tak jako tak napsat, proč ne?

Jak jsou na tom očekávané výjimky?

def test_zerodivision():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1/0
    assert excinfo.value.args[0] == 'integer division or modulo by zero'

Co třeba almostEqual?

Tato metoda a jí podobné chybí. Ale nikdo nezakazuje použití jiných balíků, třeba nose.

from nose.tools import assert_almost_equals

def test():
    assert_almost_equals(1.234, 1.2345, places=4)

Pytest s obyčejným assertem dokáže vše, co je potřeba, aniž byste přišli o jakoukoliv informaci. Dokonce vám dovolí rozšířit assert o vaše informace. Takže při volání assert s vašimi instancemi si můžete přidat potřebná data. Více v dokumentaci.

class Foo:
    def __init__(self, val):
        self.val = val

def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
        return [
            'Comparing Foo instances:',
            '   vals: %s != %s' % (left.val, right.val)
        ]

Výstup pak vypadá například takto:

    def test_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert Comparing Foo instances:
E            vals: 1 != 2

Fixtury

Test se však neskládá jen z testu. Pro test je potřeba, především pro integrační test, předpřipravit data, konexe a další závislosti. Pytest tuto nutnost řeší od začátku bravurním způsobem. Pravděpodobně největší páka, proč jsem vynaložil energii unittest u nás vyměnit právě za pytest.

Klasické unittesty mají metody setUp a tearDown, kterými lze předpřipravit prostředí pro testy, či po sobě uklidit. Problém začne, jakmile taková příprava nění jednoduchá. Například pokud chcete otestovat založení objednávky v e-shopu. K tomu potřebujete minimálně testovací produkt a testovacího uživatele. Takový produkt může být nějakého typu, který potřebuje nakonfigurovat. U uživatele zase potřebujete vyplněné fakturační údaje; ověřit, že se vygeneruje dobře faktura. Poměrně hodně věcí pro jeden setUp.

Pytest na to jde jinak. „Magickým“ dependency injection.

def test_order(user, product):
    # test ordering of product by user

Kde pytest získá uživatele a produkt?

Vy mu je nadefinujete.

@pytest.fixture
def user():
    return create_some_user_here()

@pytest.fixture
def product():
    return create_some_product_here()

Dobře. Ale pro tyto fixtury je potřeba databáze…

@pytest.fixture(scope='function')
def db():
    return connect_to_database()

@pytest.fixture
def user(db):
    return create_some_user_here(db)

OK. Chtěl bych, aby test končil rollbackem a nic po sobě nenechal.

@pytest.yield_fixture(scope='function')
def db():
    db = connect_to_database()
    db.begin()
    yield db
    db.rollback()
    db.close()

Za pozornost stojí parametr scope. Tímto parametrem se určuje, kde všude se má fixture cachovat. Pokud řeknu „function“, říkám tím, aby ve všech fixturách, které potřebuje jeden test, byla stejná instance databáze. Tím mohu pracovat v jedné transakci a rollbacknout. Pokud bych neurčil scope, každá fixture by měla jiné spojení s databází a nešlo by využít transakcí.

Tohle je na fixturách moc hezké. Mohou záviset sami na sobě a příprava i čistka jsou na jednom místě vedle sebe. Plus fixtury mohou být sdílené přes všechny testy a vytvářejí se pouze, pokud jsou opravdu potřeba daným testem. Osobně mi to přijde skvělé a mnohem přehlednější.

Zní dobře. Je možné se připojit k databázi jednou a pro každý test jen začít transakci?

@pytest.yield_fixture(scope='session')
def db_connection():
    db = connect_to_database()
    yield db
    db.close()

@pytest.yield_fixture(scope='function')
def db(db_connection):
    db_connection.begin()
    yield db_connection
    db_connection.rollback()

Fixtury lze skládat jak je libo. Jediné pravidlo je, že nelze, logicky, z většího scope záviset na menším. Tedy například ve fixture db_connection nelze záviset například na fixture se scope „function“. Protože by se pak tato fixture musela v db_connection nacachovat pro všechny testy, což by mohlo mít nežádoucí následky.

Mimochodem v reportu testu uvidíte, jaká instance fixtury do testu vstupovala. To je proč se hodí vidět i ty adresy instancí v paměti. Lze pak jednoduše zkontroloval, zda chybu nezpůsobuje špatná konfigurace fixtur.

Parametry

Super. Chtěl bych vyzkoušet otestování s dvěma typy uživatelů – zaregistrovaný a jednorázový nákup.

@pytest.fixture(params=['registred', 'unregistred'])
def user(request, db):
    return create_some_user_here(db, request.param)

Takhle málo stačí a všechny testy, kde se využívá fixture user, se provedou dvakrát. V reportu testu je to opět hezky vidět.

================================= FAILURES ================================
-------------------------- test_order[registred] --------------------------

user = 'registred'

    def test_order(user):
>       assert 0
E       assert 0

t.py:11: AssertionError
------------------------- test_order[unregistred] -------------------------

user = 'unregistred'

    def test_order(user):
>       assert 0
E       assert 0

t.py:11: AssertionError
========================= 2 failed in 0.01 seconds =======================

Skvělé! Bude to fungovat i když bych chtěl testovat více produktů?

No jasně! Pak se test provede pro každou kombinaci. Samozřejmě lze parametrizovat i fixtury využité jinými fixtury. V našem případě například db_connection může vracet pokaždé jinou konexi – jednou na MySQL, podruhé třeba na PostgreSQL.

Dříve testovat všechny kombinace bylo peklo. S pytestem napíšete jeden test, jednu fixture a nastavíte parametry.

V některém testu bych chtěl mít ale nějaká specifická data. Třeba v jednom testu bych chtěl, aby se mi vytvořil produkt s určitou cenou, kdežto pro ostatní případy stačí cokoliv. V unittestu lze tohle snadno, ale tady se předává jen název fixtury.

I tohle lze zařídit a běžně využíváme. Dělá se to pomocí speciálního parametru request, už jsme ho viděli výše při předávání parametrů. Je to vlastně podobná technika.

@pytest.fixture
def user(request):
    return create_some_user_here(request.function.user_name)


def test(user):
    # ...
test.user_name = 'Some name'

V requestu naleznete také atribut module či cls. Ve skutečnosti je tam toho ještě víc, mrkněte do dokumentace.

Abychom si tohle usnadnili, udělali jsme si hezký dekorátor a pomocnou funkci na mergování těchto dat. Pak vytvoření záznamů do databáze máme jednou pro všechny testy, i když každý test potřebuje trochu jiná data.

def use_data(**data):
    def wrapper(func):
        for key, value in data.items():
            setattr(func, key, value)
        return func
    return wrapper

def get_data(request, attribute_name, default_data={}):
    data = default_data.copy()
    data.update(getattr(request.module, attribute_name, {}))
    data.update(getattr(request.cls, attribute_name, {}))
    data.update(getattr(request.function, attribute_name, {}))
    return data

Poté je použití velice jednoduché a můžete definovat na modulu i třídě najednou. Data se zmergují do sebe logicky, jak by se očekávalo (modul má nejnižší prioritu a funkce přebíjí vše).

@pytest.fixture
def user(request):
    data = get_data(request, 'user_data')
    return create_some_user_here(data)

user_data = {'username': 'test', 'name': 'name'}

class UserTest:
    user_data = {'email': 'test@example.com'}

    @use_data(user_data={'name': 'Some name'})
    def test(user):
	# user.username = 'test'
	# user.email = 'test@example.com'
	# user.name = 'Some name'

Naše pomocná metoda je trochu chytřejší a dokáže pracovat i se seznamem, takže snadno můžeme říct, že uživatelů chceme pro test víc. Ještě ale ladíme detaily. Jakmile vyladíme, velká šance, že na PyPI přibude balíček. Nebo pull request do samotného pytestu. :-)

EDIT: Balíček už existuje a je k nalezení pod názvem pytest-data.

Označování testů

Půjdeme dál. Je velice vhodné mít tzv. smoke testy. Pár testů, které musí vždy projít a je jistota, že základní věci budou určitě fungovat. S pytestem velice snadno.

@pytest.mark.smoketest
def test():
    pass

Hotovo. Nyní stačí spouštět buď všechny jako doposud, nebo s parametrem -m: py.test -m smoketest. Toto označení v pytestu není, takto jste si vlastně vytvořili vlastní označení testů a můžete si je označit jak chcete.

Co překlepy?

Aby nedocházelo k chybám, lze si marky zaregistrovat, případně nikdo nebrání si vytvořit mark a importovat si ho. Lze označit také pouze jeden parametr v parametrizovaném testu.

U nás jsme si to obohatili o vlastní command line option. Udělali jsme pro jistotu opak – místo označování důležitých testů označujeme nedůležité. Tak nezapomene na nějaký důležitý test. Díky tomuto spuštění testů je raz dva a integrační tool se pak postará o kompletní funkčnost. Mimochodem dříve jsme měli jednotkové a integrační testy oddělené – nyní je máme vedle sebe a integrační je označen jako slow.

slow = pytest.mark.slow

def pytest_addoption(parser):
    parser.addoption('--skip-slow-tests', action='store_true', help='skip slow tests')

def pytest_runtest_setup(item):
    if 'slow' in item.keywords and item.config.getoption('--skip-slow-tests'):
        pytest.skip('Skipping slow test (used param --skip-slow-tests)')

Jaké testy jsou ale pomalé? Easy peasy. Stačí přidat argument --duration. Pokud zavolám například py.test --duration 10, na konci testu pytest vyreportuje 10 nejpomalejších míst. Záměrně neříkám testů, ale míst, protože report je rozdělen na část setup, call a teardown. Což se hodí; například při testování se Seleniem je inicializace prohlížeče náročné a vidím aspoň tak, kolik reálně test dlouho trval. První test v řadě tak není hendikepován.

Zašli jsme ještě dál a začali jsme označovat integrační testy, které využívají cizí službu. Pracujeme na obchodní aplikaci, která komunikuje téměř se všemi službami Seznamu. To už je velká šance, že některé rozhraní v testu na chvíli spadne. Pokud taková situace nastane, popadá nám několik integračních testů, což akorát v reportu překáží.

Dnes si testy označíme a pokud potřebné rozhraní není dostupné, test se přeskočí. Stále vidíme, pokud se tak stalo, ale nyní se nemusíme prokousávat například sto spadlými testy. A stačí tak málo…

def pytest_runtest_setup(item):
    marker = item.get_market('uses')
    if marker:
        if not any(is_not_available(arg) for arg in marker.args):
            pytest.skip('Skipping test requiring {}'.format(marker.args))

@pytest.mark.uses('penezenka', 'sklik')
def test():
    # ...

Pluginy

Ještě nekončíme. Unittesty či nose mají argument fast fail indikující, aby běh testů spadl po první chybě. Je tu super věc na ladění. Pytest má ještě lepší: last fail.

Last fail je ve skutečnosti plugin. Instalace je však velice snadná a přidá do command line další argument --lf. A cože to dělá? Spustí jen ty testy, které selhaly v posledním běhu.

Modelová situace: spustíte všechny testy. Trvají 10 minut, spadne jich 20. Provedete opravu, ale nechcete čekat celých deset minut. Nevadí, přidáte argument --lf a spustí se pouze těch 20 rozbitých. Po dokončení většinu opravíte, ale ještě 6 rozbitých testů zbývá. Provedete opravu a zase spustíte s argumentem --lf. Spustí se pouze těch posledních šest. Jakmile se dostanete na nulu, spustíte opět všechny a ověříte, zda jste nerozbili něco dalšího. Mezitím ale můžete na kafe, protože velice pravděpodobně máte hotovo.

Šikovných pluginů je celá řada. Například si můžete udělat testy na PEP8. Existují pluginy pro Flask, Django, … Taky instafail na okamžitý report. Podívejte se do dokumentace a internetu.

Propojení s unittesty

Celé to je děsně super. Ale už mám v mém projektu unittesty…

Nevadí. Pouze zahoďte unittestový či nosový runner, nakonfigurujte si pytest dle potřeb a můžete začít psát testy v novém stylu. U nás máme nakombinované Seleniové testy a žádná velká dřina to nebyla. Pytest si s unittesty poradí, dokonce lze do unittestů dostat fixture. Takto jsem tam dostal Seleniový driver:

class BaseTestCase(unittest.TestCase):
    @pytest.fixutre(autouse=True)
    def _set_driver(self, driver):
        self.driver = driver

Parametr autouse u fixture je taky dobrá věc. Využili jsme například taky při vytváření virtuálního displeje pro Selenium. Není pak potřeba někde explicitně psát, aby se taková fixture všude použila.

OK. Ale pročti si dokumentaci. Tímhle to nekončí!

Python: Pozor na deepcopy

V práci jsme řešili takový nemilý problém, kdy v určitém stavu aplikace dokázala sežrat tolik paměti, až ji museli admini otočit. Ač to není nic dobrého, měli jsme štěstí. Zjistili jsme totiž, při jaké akci se to děje. Tudíž jsme si mohli z poloviny oddychnout…

Jednalo se o operaci, kde se prováděla obrovská kopie pomocí deepcopy. Instance nebyly malé, ale ani zase tak obrovské, aby se využití paměti po operaci navýšilo o pár řádů. Nedávno jsme přidávali zpětné reference a tak jsem se na ně zaměřil. I přesto, že jsme se jim se spojením deepcopy věnovali.

Překvapivě netrvalo dlouho nalézt problém. Měli jsme takovouto strukturu:

Container
 - Item (s referencí na container)
 - Item (opět s referencí na container)
…

Při kopírování celého containeru si deepcopy se zpětnou referencí poradí hravě. Pokud jste někdy přetěžovali magickou metodu __deepcopy__, jistě víte, že tam je parametr memo. Zde se předává slovník s objekty, které už byly zkopírované. Klíč je ID staré instance a hodnota je zkopírovaná instance. Tudíž když se má kopírovat znovu stejná instance, vezme se odsud. Programátor však tohle řešit nemusí, Python se postará sám.

Problém nastane, pokud se rozhodneme kopírovat itemy postupně.

for item in container:
    new_container.append(copy.deepcopy(item))

Vidíte to také? S každou položkou se zkopíruje i container. A nejen ten – container má referenci na položky, takže se zkopírují s každou položkou všechny položky! Nejenom, že se reference úplně rozjedou, ale navíc to při velkých strukturách sežere vše, co najde.

Možná řešení jsou dvě. Buď předávat <code>memo</code>, jenže to by musel řešit container a při kopírování pouze položek odjinud by vznikal problém znovu; nebo před kopírováním položky odmazat zpětnou referenci a po operaci ji vrátit. My zvolili druhou možnost.

Tím se problém téměř vyřešil. Bohužel se stále při kopírování mírně navyšovala podezřele paměť. Hledal jsem tedy dál a  po čase jsem našel příčinu – flyweight. To je pattern, kdy je v celém programu jen jedna instance jednoho objektu. Využíváme to s úspěchem u číselníků. Představte si to jako modely v Djangu a kdykoliv si vyžádáte nějaký záznam, nedostanete celý objekt, ale jen referenci na již existující. U naší velké aplikace flyweight hodně pomohl s optimalizací.

Každopádně jsem až po letech využívání zjistil, že deepcopy nečetl vůbec nic o patternech, natož nic o flyweight, a v klidu nám tyto instance kopíroval. Stačilo do flyweight třídy implementovat metodu __deepcopy__, aby vracela self, a nyní je vše v pořádku.

Deepcopy může hodně potrápit. Před použitím si zkuste v hlavě představit, co se stane. Detailně projděte, co kopírujete. Sice vás uživatelé nepoplácají po zádech, že myslíte na vše možné, ale lepší než ve stresu řešit provozní problémy. :-)

Django signály

Nemám v úmyslu zde podrobně popisovat, jak signály fungují. Od toho tu je dokumentace. Chci pouze upozornit, že existují, lze je šikovně využít, ale taky si s nimi lze nadrobit na problémy.

K čemu signály jsou? Jednoduše k decoupling. Zrušení závislostí v kódu. Představme si to na ukázce, třeba typu e-shopu:

E-shop se skládá ze spoustu menších aplikací a každá dělá jednu svou věc. Jedna z těchto aplikací jistě bude napojení na platební bránu. Po zaplacení platby ale určitě nebude stačit pouze upravit v databázi stav platby, ale bude chtít také změnit stav objednávky, poslat zákazníkovi informaci o stavu, odeslat informace na sklad, … a spoustu dalších věcí záležící dle konkrétního e-shopu.

Klasicky by v appce payment musel být kód, který bude importovat ostatní appky e-shopu a volat jejich metody. Co se právě stalo? Appka payment není univerzální. Nejde ji dát do odděleného repozitáře a využít jinde. Je navždy spojena s konkrétním kódem e-shopu.

Jak by to vypadalo se signály? V payment appce se definuje signál, který se bude vyvolávat při změně platby. Ostatní appky budou na tento signál pouze vyčkávat. Tím bude platební modul přenositelný i do jiných aplikací. Sice je pravda, že nyní je situace opačná – ostatní appky jsou závislé. Ale… tak je to přeci správně, ne? :-)

Pro inspiraci – na BOObook.cz o produktech, krom základních atributů jako název či cena, žádná jiná appka nic neví. Nemusím tak nikde natvrdo psát, aby se po zaplacení zákazníkovi knihy zapsaly do knihovničky či poslaly mailem. Jednoduše se v appce ebook čeká na spuštění signálu o zaplacenosti a poté se zde zařídí vše potřebné.

Tím jsem docílil toho, že mohu vzít kód e-shopu krom produktů, napsat novou definici produktů třeba na zubní pasty a vše mi bude fungovat. Aniž bych musel cokoliv jiného měnit. Skvělé!

Podobně s fakturačním modulem. Nikde nevolám invoice.create_invoice(...), místo toho fakturační modul čeká na ten správný signál. Tím mohu mít dva fakturační moduly (jeden velmi základní a jeden komplexní s napojením do účetnictví) a zaměnit je dle libosti bez úpravy kódu. Dle toho, o který si zákazník řekne.

Než se ale vrhnete do zkoušení – signály nejsou řešením pro všechno. Je potřeba se zamyslet, zda využití signálu bude opravdu užitkem. Aspoň určitě se budete důkladně zamýšlet, až začnete hledat první bugy v těchto úsecích kódu. Není to zábava. :-) Nadměrné použití signálů také ztíží čitelnost kódu, protože už nebude jasné, kdy se co volá. Takže opatrně s nimi!

Co mi vadí na Pythonu

Občas mě kamarád poprosí o pomoc se zapeklitým problémem, většinou v PHP. Přijdu, pomůžu vypátrat a nezapomenu zmínit, jak je PHP nelogické, nekonzistentní, plné WTF momentů a jak jsem rád, že jsem tento svět opustil. Dokonce jsem kamaráda nahlodal natolik, že si začal o Pythonu číst. Zajímalo ho ale taky, když mám tolik výtek k PHP, kolik a jaké výtky mám k Pythonu? To mě trochu zarazilo, nikdy jsem o tom nepřemýšlel. A na nic se mi nedařilo přijít. Nevzdal jsem se, vytvořil si prázdný texťák a postupem si zapisoval co se naskytlo. Výsledkem je následující seznam…
  • Výchozí parametr ve funkci/metodě se vyhodnotí jednou při kompilaci, nikoliv při volání. Tedy pak vzniká tento problém:
>>> def foo(l=[]):
...     l.append(42)
...     print l
... 
>>> foo()
[42]
>>> foo()
[42, 42]
  • Datový typ bool je ve skutečnosti také int. Při podmínkování na datový typ se nesmí zapomenout nejprve zjišťovat, zda se nejedná o bool a teprve potom o int. Tím to nekončí, jsou tu další zajímavé vedlejší účinky:
>>> isinstance(True, int)
True
>>> True + True
2
>>> {1: 'one', True: 'true'}
{1: 'true'}
  • Líbí se mi, že si lze pomoct závorkami zalomit text bez nutnosti použití ošklivých zpětných lomítek. Bohužel to s sebou přináší možnost vznik chyb, které se velmi těžko hledají…
>>> t = ('a', 'b' 'c')
>>> len(t)
2
>>> # Tohle je ale fajn.
>>> (
...     'some very long '
...     'sentence...'
... )
'some very long sentence...'
  • Také se mi líbí, že tuple lze zapsat bez nutnosti závorek (tím pak lze zapsat například for k, v in d.items()). Jen když se někde nechtěně objeví čárka nebo se naopak na ni zapomene…
>>> 2,
(2,)
>>> (2)
2
  • Je fajn, že se mohu vybrat svobodně mezi tabulátory a mezerami. Co už ale fajn není je, že mix je povolen. Když se pak otevře soubor na chvíli v jiném editoru a nevšimnu si špatného nastavení, Python mi nezahlásí SyntaxError.
  • Chápu důvod, proč se musí explicitně k metodám psát self. Ale, opravdu by to nešlo bez toho? Nejednou jsem zapomněl self napsat.
  • Relativní importy jsou skvělá věc, ale limituje to v použití názvů. Aneb naštve když mě konečně napadne skvělý název pro soubor s mojí super třídou a o chvilku později zjistím, že mi to koliduje s nějakou knihovnou, kterou používám. To lze tedy potlačit direktivou from __future__ import absolute_import nebo použitím Pythonu 3, takže už to není takový problém.
  • str a unicode. Kolik chyb tohoto problému jsem viděl! Kéž by všichni psali tak, aby byl všude unicode a převode na str jen naprosto v nutném případě. Naštěstí tohle řeší Python 3.
  • Naneštěstí použití Pythonu 3 není tak jednoduché. Nekompatibilita je nekompatibilita a tak ne všechny používané knihovny zmigrovaly. Což je u větších projektů dost limitující (vždycky se najde knihovna, kvůli které přejít nelze).

A to je vše, víc mě nenapadá. Celkově mám Python velmi rád. Možná proto, že je to rebel; kamarád mi řekl, že Python mění jeho svět programování. Jinde jsou totiž podobné zápisy vyhodnoceny jako syntax error. Aneb díky Pythonu se mohu soustředit více na problém, který řeším, a nepřemýšlet, jak něco napsat.

Selenium + Python + Debian server

Už mi nestačí hrabat se pouze v jednotkových či integračních testech a tak si nějak dobu hraju se Selenium. S nástrojem, který dokáže z velké části nahradit i „testera klikače“. Před pár dny jsem měl potřebu si Selenium rozběhat na serveru s tím, že absolutně netuším, jestli to lze a jestli to lze udělat nějak snadno. (Mimochodem serverem mám na mysli mašinu někde bůh ví kde bez jakéhokoliv vstupně/výstupního zařízení.)

Pokud nebudu počítat občas trochu zmatené lidi v diskuzích kolem tohoto problému (na otázku jak nainstalovat Firefox na Debian serveru bez Xek jsem našel nespočet odpovědí „ty blázne, to nejde. Proč to vůbec potřebuješ, vždyť to je blbost!“), tak to udělat lze a velmi jednoduše. 

Jako první si musíme uvědomit, cože to vlastně požadujeme. Chceme použít Selenium 2.0 (Webdriver) v Pythonu na otestování webové aplikace, která nám běží na Debian serveru bez X serveru. Začněme tedy postupně řešit.

Nejprve je potřeba Python, nainstalujeme.

# apt-get install python

Super, máme. Dále potřebujeme Selenium pro Python. Selenium lze nejjednodušejí nainstalovat přes PyPI (Python Package Index), na to potřebujeme nainstalovat balík python-pip.

# apt-get install python-pip
# pip install selenium

Máme nutný základ pro jakékoliv testování pomocí Pythonu a Selenia. Dál je potřeba nějak vyřešit problém „žádný X server“. Řešeních je víc, já si však vybral XVFB. Celým jménem virtual framebuffer X server. Česky se jedná o virtuální X server. Tedy možnost spouštět grafické programy bez nutnosti fyzické podpory pro grafický výstup. Jen nic není vidět. :)

Budeme potřebovat nejen balík xvfb, ale také knihovnu pyvirtualdisplay pro Python, abychom mohli ovládat xvfb přímo z našeho testovacího kódu.

# apt-get install xvfb
# pip install pyvirtualdisplay

Nyní nám zbývá předposlední část a to nainstalovat Firefox. Ve standardních Debian repozitářích k nalezení není a tak se musí nejprve přidat další zdroj na linuxmint.

# vim /etc/apt/sources.list
// pridat radek "deb http://packages.linuxmint.com debian import"
# apt-get update
# apt-get install firefox

Tím je připraveno prostředí a můžeme konečně spustit první test funkčnosti.

from selenium import webdriver
from pyvirtualdisplay import Display

display = Display(visible=0, size=(800, 600))
display.start()

browser = webdriver.Firefox()
browser.get('http://www.google.com')
print browser.title
browser.quit()

display.stop()

Pokud vše proběhlo hladce, zbývá si už jen ukázku přepsat do Pythoních unittestů a s radostí začít testovat. :)

class SomeTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.display = Display(visible=0, size=(800, 600))
        cls.display.start()
    
    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.get('http://www.google.com')
    
    def testGoogleTitle(self):
        self.assertEqual(self.browser.title, 'Google')
    
    def tearDown(self):
        self.browser.quit()
    
    @classmethod
    def tearDownClass(cls):
        cls.display.stop()

if __name__ == '__main__':
    unittest.main(verbosity=2)

Výstup pak už je klasický, který známe:

testGoogleTitle (__main__.SomeTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 4.226s

OK

Dále je dobré si udělat nějaký vlastní wrapper pro TestCase, který se postará o rutiny. (Stále pamatujeme na DRY.) Případně ještě nevytvářet instanci Firefoxu pro každý jednotlivý test, ale pro celou třídu nebo pro všechny testy. Záleží na potřebách a preferencích.

Tento způsob se mi velmi líbí i pro desktop, protože (díky použití Webdriveru) nemusím pouštět žádný Selenium server a ani mi na monitoru nevyskakuje Firefox. Vše hezky v pozadí. A když se něco nedaří a potřebuju vidět, co se děje, stačí pouze dočasně odstranit vytváření virtuálního X serveru.

Proč jsem zvolil Webdriver?

Protože to je součást Selenia 2.0 (největší novinkou druhé verze je právě integrace Webdriveru). Tedy je to směr, kterým se tento nástroj ubírá.

Protože má objektovější API. Sice to v mých velmi jednoduchých ukázkách není dobře vidět, ale náznak tam je. Pak lze kusy kódu lépe zapouzdřit. Například vytvořit celou třídu reprezentující nějaký základní stavební kámen na webu a není třeba jeho funkčnost mít všude možně (změna v HTML se poté nerovná změně ve všech testech, ale jen někde na jednom místě).

Protože Selenium ovládá prohlížeč skrze JavaScript, kdežto Webdriver ovládá přímo prohlížeč. Na Webdriveru se podílejí všichni hlavní tvůrci prohlížečů. Díky tomu také není potřeba spouštět Selenium server.

Seriál Python profesionálně

Měl jsem chuť napsat sem něco o Pythonu. Později jsem zatoužil po víc a chtěl jsem napsat o zajímavostech v Pythonu – jak se v něm věci dělají jinak, jednodušeji, na co si dát pozor, a tak. Postupně jsem si sepisoval, co bych chtěl zmínit, když v tom jsem to celé přeskládal a přidal podstatně více věcí. Nakonec vznikl seriál.

Zkusil jsem to napsat zajímavě. Použil jsem popis nějakého problému s jeho postupným řešením od špatných řešení až po ta dobrá, kde jsem i na tom špatném ukazoval různé věci. Někomu se to líbí hodně, někomu vůbec (to hlavně puristům). Osobně jsem s výsledkem spokojen, i když to není úplně takové, jak jsem si představoval, že bude.

Ať tak či onak, pokud máte za sebou nějakou prvotní příručku Pythonu za sebou (například Dive Into Python 3, která je k nalezení i v češtině) a zatím jste můj krátký seriál o Pythonu nečetli, nezapomeňte to napravit. :)

A kdeže je? Na Zdrojáku pěkně prosím:

  • V prvním díle vás uvedu do seriálu a začnu s jednoduchými věcmi jako ternární operátor, iterace, cyklus a podobně. Ale nepřeskakujte tento díl – téměř s jistotou vše neznáte.
  • Ve druhém díle se posunu dál ke generátorům, lambda funkcím nebo například konext manageru.
  • Ve třetím díle se dozvíte pár vybraných tipů na nějaké moduly či proč si dávat pozor při definování defaultních parametrů ve funkci.
  • Ve čtvrtém díle předvedu několik návrhových vzorů, které jsou v Pythonu napsat jinak a lépe, než znáte z jiných jazyků.
  • A v posledním, pátém, díle se dozvíte, cože to je vlastně metatřída, jak ji napsat a příklady kde se dá šikovně použít.

Prosím, nepište komentáře k seriálu sem, ale k jednotlivým článkům na Zdroják, ať to je na jednom místě. Díky.