Hlavní navigace

Létající cirkus (20)

5. 12. 2002
Doba čtení: 8 minut

Sdílet

V dnešním díle seriálu věnovaném jazyku Python si v krátkosti představíme další velice užitečný balíček - Numeric Python, díky kterému dokáže Python pracovat s opravdu rozsáhlými poli dat a provádět nad nimi různé matematické operace.

CO NUMERIC PYTHON UMOŽŇUJE?

Numeric Python (zkráceně NumPy) umožňuje programátorovi pracovat s poli čísel, která mohou být svým objemem v řádech megabytů i více, přičemž vyniká velkou rychlostí i při opravdu velkých objemech dat. Všechna data, která NumPy dokáže obhospodařovat, musí být stejného typu, jmenovitě jde o různé typy celých a reálných čísel, komplexní čísla a dokonce odkazy na pythonovské objekty. Tato pole musí být homogenní, tj. žádnou hodnotu nelze vynechat.

Nad těmito poli lze provádět různé matematické funkce, ať se jedná o ty základnější, jako je sčítání nebo násobení, přes goniometrické funkce až třeba po násobení matic atd. Všechny tyto funkce jsou v terminologii Numericu nazývány univerzální funkce.

Výkonný kód balíčku je napsán v jazyce C, čímž je docíleno maximální rychlosti provádění. Samotný balíček obsahuje několik modulů. Jde především o samotné jádro umožňující práci s poli (multiarrays) a uživatelskými funkcemi (ufuncs), dále Numeric nabízí modul, obsahující funkce pro práci s maticemi obdobným způsobem jako v lineární algebře, modul pro vytváření polí, jež mají některé prvky nedefinovány, nebo modul pro aplikování rychlé Fourierovy transformace (FFT).

NUMERIC A POLE

Jak jsme si již řekli, základním stavebním kamenem celého Numericu je nový datový typ implementovaný v C – multiarray. Tato pole se chovají obdobným způsobem jako klasické sekvence v Pythonu (seznam, tuple), ale přesto nabízejí několik nových vlastností:

  • mohou mít neomezeně dimenzí a přes každou dimenzi mohou být indexována
  • jsou homogenní a všechny prvky mají stejný typ
  • jejich velikost je neměnná, ale jejich obsah je možné libovolně měnit

Pole je v Pythonu reprezentováno jako klasický objekt, který má několik atributů a metod. Lze k němu přistupovat jako ke kterékoli sekvenci pomocí indexů apod. Každé pole reprezentuje několik vlastností:

  • počet prvků (size) – jde o celkový počet prvků v celém poli, tento počet je dán již při vytvoření pole a nelze ho později změnit. Lze zjistit pomocí funkce size().
  • tvar (shape) – určuje, kterak jsou prvky uspořádány do jednotlivých rozměrů. Tuto vlastnost reprezentuje atribut shape, jedná se o tuple hodnot, které pro každou dimenzi specifikuje rozměr pole. Tvar pole lze měnit v průběhu jeho života. Je však třeba ale vždy zachovat počet prvků. Proto lze například matici typu 3/5 přetransformovat na 5/3, ale nikdy ne na 2/7.
  • počet dimenzí (rank) – pro skalár (tj. bezrozměrné pole) je roven 0, vektor 1 a matice má rank roven hodnotě 2. Počet dimenzí se rovná např. délce tuple specifikující shape, případně ho lze zjistit pomocí funkce rank()
  • typ prvků (typecode) – určuje typ jednotlivých prvků pole, vrací ho metoda typecode(). Jedná se o jediné písmeno (např. ‚I‘ pro pole skládající se z celých čísel, ‚D‘ pro komplexní čísla nebo ‚O‘ pro odkazy na Pythonové objekty).

Nové pole lze vytvořit například pomocí funkce array(). Tuto funkci použijeme, pokud budeme chtít vytvořit nové pole z nějakého již existujícího, případně z libovolné pythonovské sekvence:

>>> print array(1)                     # skalár
1
>>> print array([1, 2, 3])             # vektor
[1 2 3]
>>> print array([[1, 0], [1, 0]])      # matice typu 2/2
[[1 0]
 [1 0]]
>>> print array([[1, 0], [1, 0]], 'f') # matice reálných čísel
[[ 1.  0.]
 [ 1.  0.]]

Funkci array() můžeme předat i argument, který bude specifikovat typecode (viz poslední řádek příkladu). Tento postup se hodí, pokud známe strukturu a data, kterými chceme pole naplnit. Numeric nám ale umožňuje ještě jednu, rychlejší cestu, kterak vytvořit nové pole naplněné daty. Předpokládejme, že máme seznam čísel, ze kterých chceme vytvořit čtvercovou matici. Pak jednoduše vytvoříme z tohoto seznamu pomocí funkce array() nové pole a následně změníme jeho tvar tak, abychom získali matici požadovaných rozměrů:

>>> l = [1, 0, 0, 0, 1, 0, 0, 0, 1]

>>> matice = array(l)
>>> matice.shape = (3, 3)
>>> print matice
[[1 0 0]
 [0 1 0]
 [0 0 1]]

Tento zápis lze zkrátit pomocí funkce reshape(), které lze předat pole, ale můžeme jí předat dokonce i obyčejnou sekvenci a ona z předaného shape vytvoří nové pole:

>>> matice2 = reshape(l, (3, 3))
>>> print matice2
[[1 0 0]
 [0 1 0]
 [0 0 1]]

Kromě těchto funkcí existují i další funkce, které dokáží vytvořit pole. Například funkce zeros(), která vrátí pole iniciované samými nulami, přičemž toto pole má shape nastaveno na hodnotu prvního argumentu funkce zeros():

>>> print zeros((2, 3))
[[0 0 0]
 [0 0 0]]

Obdobně pracuje i funkce identity(), která přebírá jediný argument n a vytvoří jednotkovou matici řádu n. Numeric definuje i svou vlastní obdobu interní funkce range() s názvem arange(), která má stejné rozhraní jako range() a vrací vektor naplněný hodnotami aritmetické posloupnosti. Při volání arange() lze specifikovat i typecode výsledného vektoru. arange() dokáže navíc pracovat s reálnými čísly, což obyčejná range() neumí!

ZPŘÍSTUPNĚNÍ PRVKŮ

Každý datový typ by byl bezcenný, kdyby nad ním nebyly definovány určité operace. Numeric a jeho pole podporují širokou škálu různých funkcí a operátorů, které dokáží s poli pracovat.

Velice důležitou operací, kterou pole umožňují, je zpřístupnění prvku. Jedná se o dokonalé využití slice operací. Rozšířené slice zápisy tak, jak je známe z dnešního Pythonu, mají svůj původ právě v Numericu. Jeho autoři totiž potřebovali silný mechanismus pro indexování, a proto vývojáři v čele s Guido van Rossumem rozšířili specifikaci jazyka přesně podle jejich představ.

Představme si tedy následující pole (jde o třírozměrné pole o rozměrech 2, 3 a 4):

>>> m = reshape(arange(24), (2, 3, 4))
>>> print m
[[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]
 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]

Prvek na pozici 1, 2 a 2 získáme klasicky jako m[1, 2, 2]. Zde se pole od svých protějšků v jiných jazycích příliš neliší. Jiná situace ale nastává, pokud chceme získat určité části tohoto pole jako v následujícím příkladě:

>>> print m[1, :, :]
[[12 13 14 15]
 [16 17 18 19]]
>>> print m[:, :, ::2]
[[[ 0  2]
  [ 4  6]]
 [[12 14]
  [16 18]]]

Jak vidíte, je možné použití slice i bez udání mezí, pak platí stejná pravidla, jaká jsme si řekli u sekvencí. Všimněte si ale druhého zápisu a obzvláště dvou dvojteček před dvojkou. V tomto případě se jedná o zápis, který z celého třetího rozměru vybere pouze sudé prvky. Číslo za druhou dvojtečkou tudíž specifikuje krok. To je oblast, kde Numeric předstihl Python. Dochází zde k paradoxu, kdy přestože to specifikace jazyka obsahuje, žádný interní datový typ není možné indexovat s udaným krokem. To vše umožní až příští verze Pythonu, lze jen předeslat, že již je vypsán PEP, který toto řeší.

Podobně jako indexování s udaným krokem je další specialitou (alespoň zatím) Numericu rozšiřování symbolu tří teček (…). Pokud Numeric narazí na tento symbol uvnitř slice indexů, považuje ho za „všechny dostupné dimenze,“ což se těžko popisujem ale je to snadno pochopitelné z následujícího příkladu:

>>> print m[..., 1]
[[ 1  5  9]
 [13 17 21]]

Z tohoto zápisu je vidět, že to, co vytiskl příkaz print, je pole, jehož prvky tvoří všechny prvky pole m, které mají poslední dimenzi rovnu 1. Pokud se však v jedné slice vyskytnou dva symboly …, pak se expanduje pouze první! Případné další výskyty převede na obyčejnou dvojtečku.

Další možnost, kterou je možné ve slice používat, je slovo NewAxis. Pokud se uvede, Numeric na jeho místě vytvoří novou dimenzi. Opět si vše ukážeme na příkladu. První zápis zpřístupní prvky, které mají první dimenzi rovnu 0 a třetí rovnu 1. Jde o tři prvky, které jsou logicky vráceny jako pole mající jednu dimenzi. Pokud bychom však chtěli vytvořit matici, jejíž první sloupec tvoří tyto prvky, použijeme právě NewAxis:

>>> print m[0, :, 1]
[ 2  6 10]
>>> print m[0, :, 1, NewAxis]
[[ 2]
 [ 6]
 [10]]

Všech možných kombinací slice indexů existuje mnoho a je pouze na fantazii programátora, jak je použije. Numeric nabízí všechny možnosti klasických sekvencí a jejich množinu podstatně rozšiřuje. Pro omezený prostor se již budeme věnovat dalším operacím na poli.

OPERACE NAD POLI ČÍSEL

Začneme tím nejpodstatnějším faktem – všechny operace pracují prvek po prvku, čili pokud napíšeme mezi dvě pole *, NEJDE o násobení matic, ale o pouhé násobení prvků mezi sebou. Pokud bychom si přáli násobit matice, musíme použít funkci matrixmultiply():

>>> a = reshape(arange(4), (2, 2))
>>> b = reshape(arange(8,4,-1), (2, 2))
>>> print a + b
[[8 8]
 [8 8]]
>>> print a * b
[[ 0  7]
 [12 15]]
>>> print matrixmultiply(a, b)
[[ 6  5]
 [34 29]]

Podotkněme, že binární operátory jako + nebo * a další musí mít zarovnaný tvar, tudíž, ve zkratce řečeno, musí mít stejné rozměry. Numeric ale podporuje i určitý mechanismus opakování dané operace, tudíž je možné provést i cosi jako:

>>> a = identity(2)
>>> b = arange(2)
>>> print a + b
[[1 1]
 [0 2]]
>>> print (a * 3) + 1
[[4 1]
 [1 4]]

Pro popis tohoto mechanismu se ale musíte začíst do dokumentace Numericu, kde je vše detailně popsáno.

Všechny operátory jsou implementovány jako univerzální funkce (ufuncs), čili mají podobné rozhraní. Tudíž funkce sin() pracuje obdobně prvek po prvku jako výše uvedené sčítání apod. Navíc, každá univerzální funkce umožňuje předání ještě jednoho dalšího argumentu, do něhož se uloží výsledek. Takto jsou implementovány in-place operátory jako +=, *= atd. Ukázka praktického použití s funkcí log():

>>> m = reshape(arange(1, 10, 1, Float), (3, 3))
>>> print log(m, m)
[[ 0.          0.69314718  1.09861229]
 [ 1.38629436  1.60943791  1.79175947]
 [ 1.94591015  2.07944154  2.19722458]]
>>> print m[2, 2]
2.19722458

Protože se Numeric snaží maximálně šetřit místem, nedojde při zpřístupnění určitých prvků pomocí slice k jejich zkopírování, pouze se vytvoří odkaz na danou oblast v PŮVODNÍM poli. Proto byste neměli být překvapeni při spuštění následujícího příkladu:

CS24 tip temata

> a = zeros((3, 3))

> b = a[:2, :2]
> b += 1
> print a
[[1 1 0]
 [1 1 0]
 [0 0 0]]

Více se nám již do dnešního dílu nevešlo, protože Numeric Python je natolik propracovaný a bohatý na funkce, že by jeho popis vydal na samostatný seriál. Nicméně jako motivaci pro vaše další studium lze dodat, že obrázky z PIL lze převést na pole Numericu a zpět, pole Numericu lze ukládat a číst pomocí modulu pickle, na úrovni jazyka C lze psát nové univerzální funkce apod. Čili balíček Numeric by měl být součástí každé instalace Pythonu!

PŘÍŠTĚ

V příštím dílu se podíváme na distribuci našich programátorských dílek, na balík distutils. Tímto pokračováním také ukončíme naše povídání o Pythonu.