Nedávno jsem napsal článek o Mnoha podivnostech JavaScriptu, ve kterém jsem popisoval některé zvláštnosti tohoto jazyka a osvětlil věci, které se zdají nesmyslné, ale ve zvláštním, zvráceném smyslu, ve skutečnosti mají smysl.
Nicméně JavaScript není jediným programovacím jazykem s podivnostmi. Vlastně si nemyslím, že bych kdy narazil na jazyk, který by neměl pod pokličkou alespoň pár podivností skrytých, snad s výjimkou Brainf*cku. Ten je perfektní.
Dnes jsem se rozhodl ponořit se do Pythonu.
Nebudu opakovat některé z obecných problémů z programovacího světa, jako je dobře známý problém, kdy 0.2 + 0.1 není rovno 0.3 nebo jiné číselné podivnosti. Tyto problémy nejsou specifické pro Python a již jsem je pokryl v článku o JavaScriptu.
Existuje však jedna kouzelná věc, která stojí za zopakování:
3 > 2 > 1
Jak jsem vysvětlil v předchozím článku, v JavaScriptu toto vrátí False
, protože podmínka je vyhodnocena zleva doprava. Python je však odlišný; srovnávací operátory můžete velmi intuitivně řetězit a výše uvedená podmínka bude vyhodnocena jako (3 > 2) && (2 > 1)
, a tedy vrátí True
. Nejsem si jistý, zda to v případě Pythonu splňuje definici „podivnosti“, ale říkal jsem si, že je to stojí za zmínku.
Teď se ale ponořme do něčeho zábavnějšího.
Strings and floats may break my bones...
Jelikož jsem již nadhodil téma čísel, pojďme se podívat na toto zajímavé chování. Funguje to pouze v Pythonu 2.7, ale i tak:
print(1 < float("+inf"))
print(1 > float("+inf"))
print("1" < float("+inf"))
print("1" > float("+inf"))
Co myslíte, jaký bude výstup kódu výše? Číslo 1 je přece menší než nekonečno, že ano? Takže možná něco jako True
, False
, True
a False
?
Vůbec ne. První dva řádky jsou True
a False
, ale pak dostaneme False
a True
. A co je také divné, když to zkusíme s -inf
místo +inf
, dostaneme False
, True
a... Také False
a True
?
Ale je to ještě podivnější. Pokud zkusíme str(float("+inf"))
a str(float("-inf"))
, dostaneme "inf"
a "-inf"
. A pokud zkusíme "1" < "inf"
a "1" < "-inf"
, dostaneme True
a False
. Co se to děje? Proč je "1" < float("+inf")
rovno False
, když "1" < "inf"
je True
?
No, Python 2.7 totiž nepřetypuje druhý parametr na typ prvního, jak by se mohlo očekávat. Místo toho porovnává objekty tak, jak jsou, a z nějakého důvodu je řetězec vždy větší než desetinné číslo. Ano, dokonce i "" > 99999
vrátí True
.
Mějte ale na paměti, že v Pythonu 3 a novějších tento kód prostě vyvolá výjimku, protože už nemůžete srovnávat řetězec a desetinné číslo nativně, což je pravděpodobně lepší, jak jsme právě demonstrovali.
Zábava s booleovskými hodnotami
Nyní zkusme něco jiného. Věděli jste, že boolean je ve skutečnosti v Pythonu podtřídou integeru? Ano, isinstance(True, int)
vrátí True
.
Také je celkem zábavné, že můžete násobit řetězec číslem. "abc" * 2
vrátí "abcabc"
. S kombinací numerických booleanů můžete vytvořit vlastní krátký ternární operátor. Místo toho, abyste dělali print "Hello" if Enabled else ""
, můžete jednoduše udělat print Enabled * "Hello"
.
Nejsem si jistý, jak moc to je užitečné, ale lepší, než nic. Mimochodem, věděli jste, že v Pythonu 2.7 bývaly booleovské hodnoty měnitelné (mutable)? Ano, dříve jste mohli udělat True, False = False, True
a sledovat svět v plamenech.
Dobře, dobře, můžete říct, že to všechno je fajn, ale buď to není tak šílené, nebo to funguje pouze ve starých verzích Pythonu. Od Pythonu 3 je vše bezchybné a perfektní, že ano? Nebo ne?
Přepišme matematiku
Jak už možná víte, Python nemá skutečný koncept zapouzdření, modifikátorů přístupu, nebo něčeho takového. Každý může změnit cokoli. Co je docela šílené, je to, že můžete dokonce měnit věci z jiných knihoven, jako je tato:
import math
math.pi = 42
math.e = 69
math.tau = 0
A ano, než se zeptáte, toto funguje v Pythonu 3. V tom, jak můžete manipulovat s konstantami (které ve skutečnosti nejsou konstantami) z jakéhokoli objektu nebo modulu, se nic nezměnilo.
Pamatuji si, že když jsem chodil na vysokou školu a převážně používal C#, tohle chování jsem nesnášel natolik, že jsem dokonce v Pythonu definoval všechny své konstanty jako metody. Což... moc smysl nedává, protože ty také můžete přepsat:
class mojetrida:
def mojepi():
return 3.1415926535
mojetrida.mojepi = lambda: "Řekl jsem ne."
Takže ne, není to v tom ani žádný smysl, ale hádám, že jsem se prostě cítil lépe, když jsem alespoň nějakým způsobem chování konstant simuloval. Nebo možná jsem ani nevěděl, že jde metody takto jednoduše přepsat (opakuji, byl jsem zvyklý na C#).
Stejná čísla jsou si rovna, ale některá jsou si rovnější
x = 20
y = 20
z = x + y
a = 15
b = 25
c = a + b
print(c is z)
Jak byste očekávali, dostanete True
jako výstup. Ale teď to zkuste takto:
x = 200
y = 200
z = x + y
a = 150
b = 250
c = a + b
print(c is z)
Z nějakého důvodu najednou dostaneme False
. A co je ještě divnější, když zkusíme napřímo 400 is 400
, vrátí nám to zase očekávatelné True
.
Teď byste mohli namítnout: „To je proto, že porovnáváš podle reference. Použij místo toho běžný operátor rovnosti a všechny případy vrátí True
.“ Jasně, ale to je nuda. Místo toho se podívejme, proč srovnávání podle reference někdy funguje, ale někdy ne.
Důvodem je to, že v Pythonu jsou malá čísla obvykle ukládána do mezipaměti a opětovně používána interpretrem, chovají se téměř jako singletony. Tato celá čísla mají rozsah od -5 do 256. Důvod za touto optimalizací spočívá v tom, že malá celá čísla jsou používána v programech často, takže tím, že je ukládáme do mezipaměti a znovu je používáme, může Python ušetřit paměť a zlepšit výkon.
Takže když jsme porovnávali reference pro proměnné obsahující číslo 40, byl to skutečně stejný objekt v paměti jak pro c
, tak pro z
. Ale to neplatilo pro číslo 400.
Jedno upozornění: výše uvedený příklad nefunguje na https://pythonsandbox.com/ a možná na některých dalších nestandardních instalacích. Nejsem si zcela jist, jaká jsou specifika a kdy přesně se optimalizace spouští, ale je to rozhodně zajímavé chování.
Ale ano, dalo by se říct, že porovnávat čísla podle reference je asi špatný nápad. Kdo by to řekl?
(AKTUALIZACE: Kolega říká, že chování operátoru is
pro čísla je ve skutečnosti nedefinováno. Proto se chování může mírně lišit v různých nastaveních nebo verzích.)
K nekonečným referencím a ještě dál!
Než se dostaneme k tomu hlavnímu, chci se podívat na poslední podivnost, která je podle mě zajímavá, i když pravděpodobně ne moc praktická.
Víte, jak některé objektové topologie mohou tvořit uzavřené grafy? Objekt může mít atribut odkazující sám na sebe a tak dále; to není nic nového. No, v Pythonu můžete mít uzavřené seznamy s prakticky nekonečnou rekurzí. Ano, to můžete udělat v téměř jakémkoli programovacím jazyce pomocí nějaké formy obecné třídy obsahující nespecifické objekty, které se chovají jako pole, které později vyplníte odkazem na sebe sama. Ale v Pythonu to můžete udělat bez toho všeho, prostě použijete:
mujlist = [42, "hello", mujlist]
Říkal jsem si, že to stojí za zmínku.
Ale teď se podívejme na nějakou jinou magii s poli:
arr_2D = [[0]*4]*4
print(arr_2D)
arr_2D[1][1] = 1
print(arr_2D)
Co si myslíte, že bude výstupem kódu výše? Zkuste se nad tím zamyslet. Počkám.
První výpis je snadný, je to jen [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
. Ale jak bude vypadat druhý výpis?
Měníme jen druhé číslo ve druhém poli, že? Takže šestá nula výše uvedeného výstupu se přepíše na 1, a to je všechno, že?
Kdepak, druhý výstup bude:
[[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]]
Co se to sakra děje?
No, Python zjevně opravdu rád ukládá věci pomocí reference, kdykoliv má příležitost.
První řádek ve skutečnosti nevytvoří 2D pole, ale pouze čtyři reference na stejné pole, jeden po druhém. Co potřebujete udělat, je vytvořit pole takto:
arr_2D = [[0]*4 for i in range(4)]
Pak vše funguje podle očekávání.
Pojďme si promluvit o mutabilních argumentech
Tak jo, musíme se podívat na kostlivce ve skříni. Asi jste už slyšeli o něčem, co se nazývá mutabilní výchozí argumenty. Co to je? No, zjednodušeně řečeno, výchozí argumenty v Pythonu jsou evaluovány při definici funkcí, což znamená přesně jednou. Jsou pak sdíleny, když jsou funkce skutečně volány.
Je to trochu podobné jako použití klíčového slova static
v jakémkoli jazyce podobném C, i když pouze pro mutabilní typy, jak později demonstruji. Když mutujete argument v těle funkce, získáte mutovanou verzi proměnné ve všech následujících voláních. Nejběžnější příklad používá výchozí seznam, takto:
def add(element, array=[]):
array.append(element)
return array
print(add(42))
print(add(42))
Výstup:
[42]
[42, 42]
Teď byste mohli říct: „To je docela cool; mohu implementovat počítadlo, které je sdílené napříč všemi voláními, že?“ Právě, že ne. Čísla jsou imutabilní, neměnná, takže tohle nebude fungovat:
def do_something(txt, counter = 0):
counter = counter + 1
print(txt)
print("Counter: " + str(counter))
do_something("Doing a task...")
do_something("Doing a task...")
Výstup:
Doing a task...
Counter: 1
Doing a task...
Counter: 1
A tohle je přesně to, co je na tom dle mého názoru tak problematické. Bere to ty nejhorší vlastnosti statických proměnných bez implementace těch nejlepších, a pak je z toho neintuitivní zmatek, který nemá využití mimo několika specifických případů, jako je semi-globální cache.
A proto je běžnou praxí v Pythonu definovat výchozí argumenty jako None a pak je inicializovat v samotném těle funkce. Ale tento přístup, ač účinný, může být trochu neohrabaný a může vést k opakování kódu na začátku každé funkce, která používá výchozí argumenty.
Existuje Návrh na zjednodušení syntaxe, takže by mohlo v budoucnu být možné alespoň napsat něco jako array ??= []
. To neřeší hlavní problém, ale aspoň by to kód trochu zjednodušilo. Ale dokud takový návrh nebude přijat a implementován, musíme se smířit se spoustou ošklivých podmínek na začátku každé funkce.
Scopy a late binding
Mám tu ještě jednu věc, kterou bych chtěl pořešit, a to je způsob, jakým Python váže hodnoty proměnných použitých v blocích a funkcích, což používá prakticky opačnou ideologii výchozích argumentů. Víte, Python používá „Late binding“ (pozdní vázání), což znamená, že proměnné jsou vázány v době vykonávání funkce, nikoli v době jejich definice. Tak se ale rozhodni, Pythone!
Jelikož Python nevyžaduje definice proměnných před jejich prvním použitím jako některé jiné jazyky, je pochopitelné, že vázání je trochu volnější, a můžete udělat něco takového:
for x in range(43):
y = x
print(x)
print(y)
Výstup:
42
42
Ano, oba printy fungují; obě proměnné jsou vázány mimo samotný blok for
. Je to trochu divné pro ty, kteří přicházejí z jazyků C, ale alespoň to umožňuje nějakou extra flexibilitu při breaku z cyklů a jiných bloků - můžete pak použít proměnné z těchto vnořených bloků.
Teď se však ponořme ještě hlouběji do oceánu šílenství. Řekněme, že máte něco takového:
def make_functions():
functions = []
for i in range(5):
def fun():
return 'X je: ' + str(i)
functions.append(fun)
return functions
for f in make_functions():
print(f())
Co si myslíte, že bude výstup tohoto skriptu? Intuitivně by to mělo být 0, 1, 2, 3 a 4, že? Budeme mít pět funkcí, z nichž každá bude mít svou vlastní uzavřenou proměnnou k výpisu, že?
No právě, že ne. Dostaneme číslo 4 vytištěné pětkrát. Hodnoty proměnných používaných v bloku jsou hledány ve vnějším scope v době vykonání funkce při volání. Ale to už je smyčka dokončena a proměnná je ponechána s její konečnou hodnotou 4.
Tohle je něco, co se může stát i v jiných jazycích. Zejména JavaScript funguje podobně a umožňuje vám vybrat si, jak pracovat se scopy pomocí var
nebo let
. V C# mohou být neintuitivní některé věci s lambda funkcemi a raději ani nezačínejme s bindováním proměnných v něčem jako LUA. Každý jazyk má své vlastní zvláštnosti.
Ale mám tu pro vás ještě jeden hlavolam - je od kolegy, který je odborník na Python:
x = 10
def a():
print(x)
def b():
print(x)
x += 1
print(x)
a()
b()
Co bude zde výstupem?
Je to nějaký trik? Určitě to bude 10, 10 a 11, že? Neexistuje žádný jiný způsob, jak by to mohlo být něco jiného, nebo ano?
Překvapení, dostanete 10 a pak... UnboundLocalError.
Řeknu vám, co se děje, a je to naprosto šílené. V metodě a
odkazujeme x
mimo rozsah funkce. To je v pořádku, můžeme to udělat. Jenom čteme hodnotu. Ale v metodě b
nejenom, že odkazujeme na číslo x
, ale také měníme hodnotu. Python vidí „aha, budeme nastavovat proměnnou x
, to je tady nová proměnná v rozsahu funkce. A jaká by měla být hodnota? Aha, ano, inkrementovaná hodnota x
. Toho x
, které máme v rozsahu funkce. Toho nového x
, které se nyní snažíme deklarovat.“
Jinými slovy, Python kód vidí spíše takto:
x = 10
def a():
print(x)
def b():
print(x2)
x2 += 1
print(x2)
a()
b()
Rozdíl mezi metodou a
a b
spočívá v tom, že a
pouze ČTE hodnotu. Python to vidí a nepřidává nové x
do rozsahu funkce, jako tomu je ve funkci b
.
A proč nefunguje alespoň první výpis v metodě b
předtím, než se pokusíme změnit hodnotu x
? No, Python deklaruje proměnné funkce předem. Podívá se, jaké proměnné jsou použity, a poté vykoná kód. První výpis tedy již odkazuje na "nové" x
, které ještě nemá hodnotu. Pokud chcete odkazovat na staré x
, musíte použít klíčové slovo global
.
Docela divoké, řekl bych. A opět opačná ideologie oproti tomu, o čem jsme mluvili dříve - oproti Late bindingu. Je to skoro jako by si Python dělal v různých scénářích, co se mu zlíbí. Ano, existují pravidla a je to deterministické. Ale možná ta pravidla nejsou úplně intuitivní. Co si o tom myslíte?
Závěrečné slovo
Takže ano. Python má své zvláštnosti stejně jako jakýkoli jiný jazyk. A možná je tak přístupný právě díky těmto zvláštnostem a protože se nemusíte příliš starat o rozsah svých proměnných. Nebo možná strávíte nespočet nocí laděním něčeho, protože jste si mysleli, že se nemusíte příliš starat o rozsah svých proměnných.
Kdo ví.
Každopádně si můžete přečíst o některých dalších zvláštnostech Pythonu zde: https://python-quirks.readthedocs.io/en/latest/
Přidat komentář