Jedná se o starší verzi knihy, která je nyní známá pod názvem Think Python. Možná si raději přečtete novější verzi.
Jak myslet jako informatik |
Kapitola 15
15. Jak myslet jako informatik?1 Kompozice
Již jste viděli několik příkladů kompozice. jedním z prvních příkladů bylo použití volání metody jako součásti výrazu. Dalším příkladem je vnořená struktura příkazů;příkaz if můžete vložit do cyklu while, do dalšího příkazu if a tak dále.
Když jste viděli tento vzor a naučili jste se o seznamech a objektech,nemělo by vás překvapit, že můžete vytvářet seznamy objektů. Můžete také vytvářet objekty, které obsahují seznamy (jako atributy); můžete vytvářet seznamy, které obsahují seznamy; můžete vytvářet objekty, které obsahují objekty; a tak dále.
V této a příští kapitole se podíváme na některé příklady těchto kombinací na příkladu objektů Card.
15.2 Karetní objekty
Pokud neznáte běžné hrací karty, nyní je vhodná chvíle pořídit si balíček, jinak by tato kapitola nemusela dávat velký smysl. v balíčku je padesát dva karet, z nichž každá patří do jedné ze čtyřbarev a jedné ze třinácti řad. Barvy jsou piky, srdce, kára a kříže (v bridžovém pořadí sestupně). Hodnosti jsou eso, 2, 3, 4, 5,6, 7, 8, 9, 10, spodek, dáma a král. V závislosti na hře, kterou hrajete, může být hodnost esa vyšší než král nebo nižší než 2.
Chceme-li definovat nový objekt, který bude reprezentovat hrací kartu, je zřejmé, jaké by měly být jeho atributy: hodnost aobsah. Není tak zřejmé, jakého typu by tyto atributy měly být. Jednou z možností je použít řetězce obsahující slova jako „Spade“ pro barvy a „Queen“ pro hodnosti. Jedním z problémů této implementace je, že by nebylo snadné porovnat karty, aby se zjistilo, která má vyšší hodnost nebo barvu.
Alternativou je použít celá čísla pro zakódování hodností a barev.Pod pojmem „zakódovat“ nemyslíme to, co si někteří lidé myslí, tedy zašifrovat nebo převést do tajného kódu. Počítačový vědec pod pojmem „kódovat“ rozumí „definovat mapování mezi posloupností čísel a položkami, které chci reprezentovat“. Například:
Pady | -> | 3 |
Srdce | -> | 2 |
Diamanty | -.> | 1 |
Kluby | -> | 0 |
Zřejmou vlastností tohoto mapování je, že obleky se mapují na celá čísla v pořadí, takže můžeme porovnávat barvy porovnáváním celých čísel. Mapování pro barvy je poměrně zřejmé; každá z číselných řad se mapuje na odpovídající celé číslo a pro lícové karty:
Jack | -> | 11 |
Queen | -> | 12 |
Král | -> | 13 |
Důvod, proč pro tato mapování používáme matematický zápis, je ten, že nejsou součástí programu Python. Jsou součástí návrhu programu, ale v kódu se nikdy explicitně neobjevují. Definice třídy pro typ Card vypadá takto:
třída Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
Jako obvykle poskytujeme inicializační metodu, která přijímá nepovinnýparametr pro každý atribut. Výchozí hodnota barvy je0, což představuje Clubs.
Chceme-li vytvořit kartu, zavoláme konstruktor Card ssuit a rankem požadované karty.
threeOfClubs = Card(3, 1)
V další části zjistíme, kterou kartu jsme právě vytvořili.
15.3 Atributy třídy a metoda __str__
Abychom mohli objekty Card vypsat tak, aby je lidé mohli snadno číst, chceme namapovat celočíselné kódy na slova. Přirozeným způsobem, jak toho dosáhnout, jsou seznamy řetězců. Tyto seznamy přiřadíme atributům třídy na začátku definice třídy:
třída Card:
suitList =
rankList =
#init metoda vynechána
def __str__(self):
return (self.rankList + “ of “ +
self.suitList)
Atribut třídy je definován mimo jakoukoli metodu a lze k němu přistupovat z kterékoli metody třídy.
Uvnitř __str__ můžeme pomocí suitList a rankList mapovat číselné hodnoty suit a rank na řetězce.Například výraz self.suitList znamená „použít atribut suit z objektu self jako indexdo atributu třídy s názvem suitList a vybrat příslušný řetězec.“
Důvodem pro „narf“ v prvním prvku v rankList je fungovat jako držák místa pro nultý prvek seznamu, který by neměl být nikdy použit. Jediné platné hodnosti jsou 1 až 13. Tentozbytečný prvek není zcela nezbytný. Mohli jsme začít na 0,jako obvykle, ale je méně matoucí kódovat 2 jako 2, 3 jako 3 atd.
Pomocí dosavadních metod můžeme vytvářet a tisknout karty:
>>> card1 = Card(1, 11)
>>> print card1
Jack of Diamonds
Atributy třídy jako suitList jsou společné všem objektům Card. Výhodou je, že pro přístup k atributům třídy můžeme použít libovolný Cardobject:
>>> card2 = Card(1, 3)
>>> print card2
3 of Diamonds
>>> print card2.suitList
Karty
Nevýhodou je, že pokud změníme atribut třídy, ovlivní to každou její instanci. Pokud se například rozhodneme, že „Jack of Diamonds“ by se ve skutečnosti měl jmenovat „Jack of Swirly Whales“, můžeme udělat toto:
>>> card1.suitList =“Vířivé velryby“
>>> print card1
Jack of Swirly Whales
Problém je v tom, že ze všech kár se právě stalyVířivé velryby:
>>> print card2
3 of Swirly Whales
Obvykle není dobré měnit atributy tříd.
15.4 Porovnávání karet
Pro primitivní typy existují podmíněné operátory(<, >, == atd.)které porovnávajíhodnoty a určují, kdy je jedna větší než, menší než nebo rovna druhé. U uživatelsky definovaných typů můžeme přepsat chování vestavěných operátorů pomocí metody s názvem__cmp__. Podle konvence má __cmp__ dva parametry, self a other, a vrací1, pokud je první objekt větší, -1, pokud je větší druhý objekt, a 0, pokud jsou si rovny.
Některé typy jsou zcela uspořádané, což znamená, že můžete porovnat libovolné dva prvky a určit, který je větší. Například celá číslaa čísla s pohyblivou řádovou čárkou jsou zcela uspořádané. Některé množiny jsou neuspořádané, což znamená, že nelze smysluplně říci, že jeden prvek je větší než druhý. Například ovoce je neuspořádané, což je důvod, proč nemůžete porovnávat jablka a pomeranče.
Množina hracích karet je částečně uspořádaná, což znamená, že někdy můžete karty porovnávat a někdy ne. Například víte, že křížová trojka je vyšší než křížová dvojka a kárová trojka je vyšší než křížová trojka. Ale která z nich je lepší, křížová trojka nebo kárová dvojka? Jedna má vyšší hodnotu, ale druhá mávyšší barvu.
Abyste mohli karty porovnat, musíte se rozhodnout, co je důležitější, zda hodnota nebo barva. Upřímně řečeno, volba je libovolná. Pro účely výběru řekneme, že barva je důležitější, protože nový balíček karet přichází roztříděnýse všemi kříži pohromadě, po nich následují všechny kára a tak dále.
S tímto rozhodnutím můžeme napsat __cmp__:
def __cmp__(self, other):
# check the suits
if self.suit > other.suit: return 1
if self.suit < other.suit: return -1
# suits are same… check ranks
if self.rank > other.rank: return 1
if self.rank < other.rank: return -1
# ranky jsou stejné… je to remíza
return 0
V tomto pořadí se esa objevují níže než dvojky (2s).
Jako cvičení upravte __cmp__ tak, aby esa byla řazena výše než králové.
15.5 Balíčky
Když už máme objekty reprezentující karty, dalším logickým krokem je definovat třídu reprezentující balíček. Paluba se samozřejmě skládá z karet, takže každý objekt Paluba bude obsahovat jako atribut seznam karet.
Následující text je definicí třídy Paluba. Inicializační metoda vytvoří atribut karty a vygeneruje standardní sadu padesáti dvou karet:
třída Deck:
def __init__(self):
self.cards =
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))
Nejjednodušší způsob, jak naplnit balíček, je pomocí vnořené smyčky. Vnější smyčka vyjmenovává barvy od 0 do 3. Vnitřní smyčka vyjmenovává ranky od 1 do 13. Protože vnější smyčka se iteruje čtyřikrát a vnitřní smyčka třináctkrát, celkový počet provedení těla je padesát dva (třináctkrát čtyři). Každá iteracevytvoří novou instanci karty s aktuální barvou a hodností a připojí tuto kartu do seznamu karet.
Metoda append funguje na seznamech, ale samozřejmě ne na tuplech.
15.6 Tisk balíčku
Jako obvykle, když definujeme nový typ objektu, chceme metodu, která vypíše obsah objektu.Pro vypsání balíčku procházíme seznam a vypisujeme každou kartu:
třída Deck:
…
def printDeck(self):
for card in self.cards:
print card
Zde a od nynějška se elipsa (…) naznačuje, že jsme ostatní metody třídy vynechali.
Jako alternativu k printDeck bychom mohli pro třídu Deck napsat metodu __str__. Výhodou metody __str__ je, že je flexibilnější. Namísto pouhého vytištění obsahu objektu generuje řetězcovou reprezentaci, se kterou mohou ostatní části programu manipulovatpřed vytištěním nebo ji uložit pro pozdější použití.
Tady je verze metody __str__, která vrací řetězcovou reprezentaci třídy Deck.Aby dodala trochu šmrncu, uspořádá karty do kaskády, kde je každá karta odsazena o jedno místo více než karta předchozí:
třída Deck:
…
def __str__(self):
s = „“
for i in range(len(self.cards)):
s = s + „“*i + str(self.cards) + „\n“
return s
Tento příklad demonstruje několik funkcí. Za prvé, namísto procházení souboru self.cards a přiřazení každé karty do proměnné používáme i jako proměnnou cyklu a index do seznamu karet.
Za druhé, používáme operátor násobení řetězců, abychom každou kartu odsadili o jedno místo více než poslední. Výraz““*i dává počet mezer rovný aktuální hodnotě i.
Zatřetí, místo příkazu print k vypsání karet použijeme funkci str. Předání objektu jako argumentu funkcistr je ekvivalentní vyvolání metody __str__ na objektu.
Nakonec používáme jako akumulátor proměnnou s. Zpočátku je s prázdný řetězec. Při každém průchodu cyklem se vygeneruje nový řetězec, který se spojí se starou hodnotou sto a získá novou hodnotu. Když smyčka skončí, s obsahujeúplnou řetězcovou reprezentaci decku, která vypadá takto:
>>> deck = Deck()
>>> print deck
Ace of Clubs
2 of Clubs
3 of Clubs
4 of Clubs
5 of Clubs
6 of Clubs
7 of Clubs
8 of Clubs
9 of Clubs
10 of Clubs
Jack of Clubs
Queen of Clubs
Král of Clubs
Ace of Diamonds
A tak dále. Přestože se výsledek zobrazí na 52 řádcích, jedná se o jeden dlouhý řetězec, který obsahuje nové řádky.
15.7 Zamíchání balíčku
Je-li balíček dokonale zamíchán, pak je stejně pravděpodobné, že se jakákoli karta objeví kdekoli v balíčku, a na jakémkoli místě v balíčku je stejně pravděpodobné, že bude obsahovat jakoukoli kartu.
Pro zamíchání balíčku použijeme funkci randrangez modulu random. Se dvěma celočíselnými argumenty,a a b, vybere funkce randrange náhodné celé číslo vrozsahu a <= x < b. Protože horní hranice je striktně menší než b, můžeme jako druhý argument použít délku seznamu a zaručeně získáme legální index.Například tento výraz vybere index náhodné karty v balíčku:
random.randrange(0, len(self.cards))
Snadný způsob, jak zamíchat balíček, je projít karty a vyměnit každou kartu za náhodně vybranou. Je možné, že karta bude prohozena sama se sebou, ale to je v pořádku. Ve skutečnosti, pokud bychom tuto možnost vyloučili, bylo by pořadí karet méně než zcela náhodné:
třída Deck:
…
def shuffle(self):
import random
nCards = len(self.cards)
for i in range(nCards):
j = random.randrange(i, nCards)
self.cards, self.cards = self.cards, self.cards
Namísto toho, abychom předpokládali, že v balíčku je padesát dva karet, zjistíme skutečnou délku seznamu a uložíme ji do nCards.
Pro každou kartu v balíčku vybereme náhodnou kartu z karet, které ještě nebyly zamíchány. Poté vyměníme aktuální kartu (i) za vybranou kartu (j). Pro výměnu karet použijeme přiřazení tuple, jako v části 9.2:
self.cards, self.cards = self.cards, self.cards
Jako cvičení přepište tento řádek kódubez použití sekvenčního přiřazení.
15. Vyměňte karty za jiné.8 Odstranění a rozdání karet
Další metoda, která by byla užitečná pro třídu Deck, je removeCard, která přijme kartu jako argument, odstraní ji a vrátí True, pokud karta byla v balíčku, a Falsev opačném případě:
třída Deck:
…
def removeCard(self, card):
if card in self.cards:
self.cards.remove(card)
return True
else:
return False
Operátor in vrací true, pokud je první operand v thesecond, což musí být seznam nebo tuple. Pokud je prvním operandemobjekt, Python použije metodu __cmp__ objektu k určeníkvality s položkami v seznamu. Protože metoda __cmp__ ve tříděCard kontroluje hlubokou rovnost, metoda removeCardkontroluje hlubokou rovnost.
Pro rozdání karet chceme odebrat a vrátit vrchní kartu.Metoda pop seznamu poskytuje pohodlný způsob, jak to udělat:
třída Deck:
…
def popCard(self):
return self.cards.pop()
Ve skutečnosti pop odstraní poslední kartu v seznamu, takže neefektivně rozdáváme ze spodní části balíčku.
Jednou z dalších operací, kterou budeme pravděpodobně chtít, je logická funkceisEmpty, která vrací true, pokud balíček neobsahuje žádné karty:
třída Deck:
…
def isEmpty(self):
return (len(self.cards) == 0)
15.9 Slovníček
kódovat Reprezentovat jednu množinu hodnot pomocí jiné množiny hodnot vytvořením mapování mezi nimi. class attribute Proměnná, která je definována uvnitř definice třídy, ale mimo jakoukoli metodu. Atributy třídyjsou přístupné z libovolné metody třídy a jsou sdíleny všemi instancemi třídy. akumulátor Proměnná používaná v cyklu ke kumulaci řady hodnot, například jejich spojením do řetězce nebo přičtením k průběžnému součtu.
Jedná se o starší verzi knihy, která je nyní známá pod názvem Think Python. Možná si raději přečtete novější verzi.
Jak myslet jako informatik |
.