Tämä on vanhempi versio kirjasta, joka tunnetaan nyt nimellä Think Python. Saatat lukea mieluummin uudempaa versiota.
How to Think Like a Computer Scientist |
Luku 15
15.1 Kompositio
Olet jo nähnyt useita esimerkkejä kompositiosta.Yksi ensimmäisistä esimerkeistä oli metodikutsun käyttäminen osana lauseketta. Toinen esimerkki on lausekkeiden sisäkkäinen rakenne;voit sijoittaa if-lausekkeen while-silmukan sisälle, toisen if-lausekkeen sisälle ja niin edelleen.
Kun olet nähnyt tämän mallin ja oppinut listoista ja objekteista,sinun ei pitäisi olla yllättynyt kuullessasi, että voit luoda listojaobjekteista. Voit myös luoda objekteja, jotka sisältävät listoja (attribuutteina); voit luoda listoja, jotka sisältävät listoja; voit luoda objekteja, jotka sisältävät objekteja ja niin edelleen.
Tässä ja seuraavassa luvussa tarkastelemme joitakin esimerkkejä näistäyhdistelmistä käyttäen esimerkkinä Card-objekteja.
15.2 Korttiobjektit
Jos et tunne tavallisia pelikortteja, nyt olisi hyvä hetki hankkia korttipakka, tai muuten tässä luvussa ei ehkä ole paljonkaan järkeä.pakassa on 52 korttia, joista kukin kuuluu johonkin neljästä lajista ja johonkin kolmestatoista rivistä. Värit ovat pata, hertta, ruutu ja risti (bridgessä alenevassa järjestyksessä). Rivit ovat ässä, 2, 3, 4, 5, 6, 7, 8, 9, 10, jätkä, kuningatar ja kuningas. Riippuen pelattavasta pelistä, ässän arvo voi olla korkeampi kuin kuninkaan tai matalampi kuin 2.
Jos haluamme määritellä uuden objektin edustamaan pelikorttia, on selvää, mitkä attribuutit pitäisi olla: arvo ja väri. Ei ole yhtä selvää, minkä tyyppisiä attribuuttien pitäisi olla. Yksi mahdollisuus on käyttää merkkijonoja, jotka sisältävät sanoja kuten ”Spade” (pata) värille ja ”Queen” (kuningatar) arvolle. Ongelmana tässä toteutuksessa on se, että kortteja ei olisi helppo verrata keskenään sen selvittämiseksi, kummalla kortilla on korkeampi arvo tai väri.
Vaihtoehtona on käyttää kokonaislukuja arvoasteiden ja värien koodaamiseen.Koodaamisella emme tarkoita sitä, mitä jotkut luulevat, eli salakirjoittamista tai kääntämistä salaiseksi koodiksi. Tietojenkäsittelytieteilijät tarkoittavat ”koodaamisella” sitä, että ”määrittelen yhdistelmän numerosarjan ja edustamieni kohteiden välille”. Esimerkiksi:
Pata | -> | 3 |
Hertta | -> | 2 |
Timantit | -> | 1 |
Kerhot | -> | 0 |
Yksilöinen piirre tässä kartoituksessa on se, että puvut kartoitetaan kokonaislukuihin järjestyksessä, joten voimme verrata pukuja vertaamalla kokonaislukuja. Rivejä koskeva kartoitus on melko ilmeinen; kukin numeerinen rivi vastaa vastaavaa kokonaislukua, ja kuvakorttien osalta:
Jack | -> | 11 |
Queen | -> | 12 |
Kuningas | – – | |
-> | 13 | |
Syy siihen, että käytämme matemaattista merkintätapaa näille vastaavuuksille on se, että ne eivät ole osa Python-ohjelmaa. Ne ovat osa ohjelmasuunnittelua, mutta ne eivät koskaan näy eksplisiittisesti koodissa. Card-tyypin luokkamääritelmä näyttää tältä:
class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
Tavanomaiseen tapaan tarjoamme alustusmetodin, joka ottaa valinnaisenparametrin jokaiselle attribuutille. Maun oletusarvo on0, joka edustaa ristiä.
Luoaksemme kortin, kutsumme Card-konstruktoria haluamamme kortin puvun ja sijan kanssa.
threeOfClubs = Card(3, 1)
Seuraavassa osiossa selvitämme, minkä kortin juuri teimme.
15.3 Luokka-attribuutit ja metodi __str__
Jotta voimme tulostaa Card-oliot helposti luettavaan muotoon, haluamme kuvata kokonaislukukoodit sanoiksi. Luonnollinen tapa tehdä se on käyttää merkkijonoluetteloita. Määritämme nämä listat classattribuuteille luokkamäärittelyn alussa:
class Card:
suitList =
rankList =
#init-metodi jätetty pois
def __str__(self):
return (self.rankList + ” of ” +
self.suitList)
Luokan attribuutti määritellään minkään metodin ulkopuolella, ja sitä voidaankäyttää mistä tahansa luokan metodista.
Sisällä __str__ voimme käyttää suitListiä ja rankListiä kuvaamaan suitin ja rankin numeerisia arvoja merkkijonoiksi.Esimerkiksi lauseke self.suitList tarkoittaa ”käytä olion self attribuuttia suit indeksinä luokan suitList-nimiseen attribuuttiin ja valitse sopiva merkkijono”.”
Syy ”narf”-merkintään rankList-olion ensimmäisessä elementissä on se, että se toimii paikanvartijana listan nollanneljännelle elementille, jota ei pitäisi koskaan käyttää. Ainoat kelvolliset rankit ovat 1-13. Tämä hukkaanheitetty elementti ei ole täysin tarpeellinen. Olisimme voineet aloittaa 0:lla, kuten tavallisesti, mutta on vähemmän hämmentävää koodata 2 numeroksi 2, 3 numeroksi 3 ja niin edelleen.
Voimme tähänastisilla metodeilla luoda ja tulostaa kortteja:
>>> card1 = Card(1, 11)
>>>> print card1
Ruutujakki
Luokan attribuutit, kuten suitList (väriLuettelo)
jaetaan kaikissa Cardobjekteissa. Tämän etuna on, että voimme käyttää mitä tahansa Cardobjectiä luokan attribuuttien käyttämiseen:
>>>> card2 = Card(1, 3)
>>>>> print card2
3 ruutua
>>>> print card2.suitList
Diamonds
haittapuolena on se, että jos muutamme luokan attribuuttia, se vaikuttaa kaikkiin luokan instansseihin. Jos esimerkiksi päätämme, että ”Jack of Diamonds” pitäisi oikeasti olla nimeltään ”Jack of Swirly Whales”, voisimme tehdä näin:
>>>> card1.suitList = ”Swirly Whales”
>>> print card1
Jack of Swirly Whales
Ongelma on se, että kaikista ruutuista tuli juuriSwirly Whales:
>>> print card2
3 of Swirly Whales
Luokan attribuutteja ei yleensä kannata muuttaa.
15.4 Korttien vertailu
Alkeistyypeille on olemassa ehdollisia operaattoreita(<, >, == jne.)jotka vertailevatarvoja ja määrittävät, milloin toinen on suurempi, pienempi tai yhtä suuri kuin toinen. Käyttäjän määrittelemille tyypeille voimme ohittaa sisäänrakennettujen operaattoreiden käyttäytymisen tarjoamalla metodin nimeltä__cmp__. Konvention mukaan __cmp__:llä on kaksi parametria, self ja other, ja se palauttaa1, jos ensimmäinen objekti on suurempi, -1, jos toinen objekti on suurempi, ja 0, jos ne ovat yhtä suuret.
Jotkut tyypit ovat täysin järjestettyjä, mikä tarkoittaa, että voit verrata mitä tahansa kahta elementtiä ja kertoa, kumpi on suurempi. Esimerkiksi kokonaisluvutja liukuluvut ovat täysin järjestettyjä. Jotkin joukot ovat järjestäytymättömiä, mikä tarkoittaa, että ei ole mielekästä tapaa sanoa, että yksi alkio on suurempi kuin toinen. Esimerkiksi hedelmät ovat järjestäytymättömiä, minkä vuoksi omenoita ja appelsiineja ei voi verrata keskenään.
Pelikorttien joukko on osittain järjestäytynyt, mikä tarkoittaa, että joskus kortteja voi verrata keskenään ja joskus ei. Esimerkiksi tiedät, että ristin kolmonen on korkeampi kuin ristin kakkonen ja ruutukolmonen on korkeampi kuin ristin kolmonen. Mutta kumpi on parempi, risti- vai ruutukolmonen? Toisella on korkeampi arvo, mutta toisella on korkeampi väri.
Tehdäksesi korteista vertailukelpoisia sinun on päätettävä, kumpi on tärkeämpi, arvo vai väri. Rehellisesti sanottuna valinta on mielivaltainen. Valinnan vuoksi sanotaan, että väri on tärkeämpi, koska uusi korttipakka tulee lajiteltuna siten, että kaikki ristit ovat yhdessä, sitten kaikki ruudut ja niin edelleen.
Kun tämä on päätetty, voimme kirjoittaa __cmp__:
def __cmp__(self, other):
# tarkista värit
if self.suit > other.suit: return 1
if self.suit < other.suit: return -1
# värit ovat samat… tarkista rivit
if self.rank > other.rank: return 1
if self.rank < other.rank: return -1
# ranks are the same… it’s a tie
return 0
Tässä järjestyksessä ässät näkyvät alempana kuin kakkoset (2s).
Muutetaan harjoituksena __cmp__ niin, että ässät ovat ylempänä kuin kuninkaat.
15.5 Korttipakat
Nyt kun meillä on objektit, jotka edustavat kortteja, seuraava looginen askel on määritellä luokka, joka edustaa pakkaa. Kansi koostuu tietysti korteista, joten jokainen Kansi-olio sisältää attribuuttina korttiluettelon.
Seuraavassa on luokkamäärittely Kansi-luokalle. Alustusmetodi luo attribuuttikortit ja generoi vakiomuotoisen viidenkymmenenkahden kortin joukon:
class Deck:
def __init__(self):
self.cards =
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))
Helpoisin tapa täyttää pakka on sisäkkäinen silmukka. Ulompi silmukka luettelee värit 0:sta 3:een. Sisempi silmukka luettelee rivit 1:stä 13:een. Koska ulompi silmukka iteroi neljä kertaa ja sisempi silmukka iteroi kolmetoista kertaa, runko suoritetaan yhteensä viisikymmentäkaksi kertaa (kolmetoista kertaa neljä). Jokaisella iteraatiokerralla luodaan uusi instanssi Cardista, jolla on nykyinen väri ja arvo,ja liitetään tämä kortti korttiluetteloon.
Liitäntä-metodi toimii listoilla, mutta ei tietenkään tupleilla.
15.6 Pakan tulostaminen
Tavanomaiseen tapaan, kun määrittelemme uuden tyyppisen objektin, haluamme metodin,joka tulostaa objektin sisällön.Tulostaaksemme kannen, käymme listan läpi ja tulostamme jokaisen kortin:
class Deck:
…
def printDeck(self):
for card in self.cards:
print card
Tässä, ja tästä eteenpäin, ellipsi (…) osoittaa, että olemme jättäneet luokan muut metodit pois.
Vaihtoehtona printDeckille voisimme kirjoittaa Deck-luokalle metodin __str__. __str__:n etuna on, että se on joustavampi. Sen sijaan, että se vain tulostaisi objektin sisällön, se tuottaa merkkijonoesityksen, jota ohjelman muut osat voivat käsitellä ennen tulostusta tai tallentaa myöhempää käyttöä varten.
Tässä on versio __str__:stä, joka palauttaa merkkijonoesityksen Deckistä.Hieman piristykseksi se järjestää kortit kaskadiksi,jossa jokainen kortti on yhden välilyönnin enemmän kuin edellinen kortti:
class Deck:
…
def __str__(self):
s = ””
for i in range(len(self.cards)):
s = s + ””*i + str(self.cards) + ”\n”
return s
Tämä esimerkki havainnollistaa useita ominaisuuksia. Ensinnäkin, sen sijaan, että kävisimme läpi self.cards ja osoittaisimme jokaisen kortin muuttujaan,käytämme i:tä silmukkamuuttujana ja indeksinä korttiluettelossa.
Toisekseen, käytämme merkkijonojen kerrannaisoperaattoria sisennyttääksemme jokaista korttia yhdellä välilyönnillä enemmän kuin edellistä. Lauseke””*i tuottaa välilyöntien määrän, joka on yhtä suuri kuin i:n nykyinen arvo.
Kolmanneksi, sen sijaan, että käyttäisimme print-komentoa korttien tulostamiseen,käytämme str-funktiota. Objektin luovuttaminen argumenttina str-funktiolle vastaa __str__-metodin kutsumista objektiin.
Viimeiseksi käytämme muuttujaa s akkumulaattorina.Aluksi s on tyhjä merkkijono. Joka kerta silmukan läpi luodaan uusi merkkijono, joka ketjutetaan vanhaan arvoon sto ja saadaan uusi arvo. Kun silmukka päättyy, s sisältää kannen täydellisen merkkijonoesityksen, joka näyttää tältä:
>>>> deck = Deck()
>>>> print deck
Risti ässä
Risti ässä
Risti ässä
Risti ässä
Risti Ässä
Risti ässä
Risti Ässä
Risti ässä
Risti Ässä
Risti ässä
Risti Ässä
Risti Ässä
Risti Ässä7 ristiä
8 ristiä
9 ristiä
10 ristiä
Risti jätkä
Risti rouva
Risti kuningas
Ruutu ässä
Ja niin edelleen. Vaikka tulos näkyy 52 rivillä, se on yksi pitkä merkkijono, joka sisältää rivinvaihdon.
15.7 Pakan sekoittaminen
Jos pakka on täydellisesti sekoitettu, mikä tahansa kortti esiintyy yhtä todennäköisesti missä tahansa pakassa, ja mikä tahansa paikka pakassa sisältää yhtä todennäköisesti minkä tahansa kortin.
Pakan sekoittamiseen käytämme randrange-funktiota satunnaismoduulista. Randrange valitsee kahdella kokonaislukuargumentilla,a ja b, satunnaisen kokonaisluvun väliltä a <= x < b. Koska yläraja on tiukasti pienempi kuin b, voimme käyttää toisena argumenttina listan pituutta, ja saamme taatusti laillisen indeksin.Esimerkiksi tämä lauseke valitsee satunnaisen kortin indeksin pakasta:
random.randrange(0, len(self.cards))
Helppo tapa sekoittaa pakka on käydä kortit läpi ja vaihtaa jokainen kortti satunnaisesti valitulla kortilla. On mahdollista, ettäkortti vaihdetaan itsensä kanssa, mutta se ei haittaa. Itse asiassa, jos sulkisimme tämän mahdollisuuden pois, korttien järjestys olisi vähemmän kuin täysin satunnainen:
class Deck:
…
def shuffle(self):
import satunnainen
nKortit = len(self.kortit)
for i in range(nKortit):
j = satunnainen.randrange(i, nKortit)
self.kortit, self.cards = self.cards, self.cards
Ennemmin kuin oletamme, että pakassa on viisikymmentäkaksi korttia, saamme listan todellisen pituuden ja tallennamme sen arvoon nCards.
Valitaan jokaiselle pakassa olevalle kortille satunnainen kortti niiden korttien joukosta, joita ei ole vielä sekoitettu. Sitten vaihdamme nykyisen kortin (i) valittuun korttiin (j). Vaihtaaksemme kortit käytämme tuplakohdistusta, kuten luvussa 9.2:
self.cards, self.cards = self.cards, self.cards
Harjoituksena kirjoita tämä koodirivi uudelleen käyttämättä sekvenssikohdistusta.
15.8 Korttien poistaminen ja jakaminen
Toinen metodi, joka olisi hyödyllinen Deck-luokassa, on removeCard, joka ottaa argumenttina kortin, poistaa sen japalauttaa True:n, jos kortti oli pakassa, ja False:
class Deck:
…
def removeCard(self, card):
if card in self.cards:
self.cards.remove(card)
return True
else:
return False
in-operaattori palauttaa true, jos ensimmäinen operandi on thesecondissa, jonka on oltava lista tai tuple. Jos ensimmäinen operandi onobjekti, Python käyttää objektin __cmp__-metodia määrittämäänequality listan kohtien kanssa. KoskaCard-luokan __cmp__ tarkistaa syvän yhdenvertaisuuden, removeCard-metodi tarkistaa syvän yhdenvertaisuuden.
Korttien jakamiseksi haluamme poistaa ja palauttaa ylimmän kortin.Listan metodi pop tarjoaa tähän kätevän tavan:
class Deck:
…
def popCard(self):
return self.cards.pop()
Tosiasiassa pop poistaa listan viimeisen kortin, joten jaamme kortteja käytännössä pakan pohjasta.
Ensimmäinen operaatio, jonka todennäköisesti haluamme, on boolean-funktioisEmpty, joka palauttaa totuuden, jos pakassa ei ole kortteja:
class Deck:
…
def isEmpty(self):
return (len(self.cards) == 0)
15.9 Sanasto
encode Yhden arvojoukon esittäminen käyttäen toista arvojoukkoa rakentamalla niiden välille kartoitus. class attribute Muuttuja, joka on määritelty luokkamääritelmän sisällä mutta minkään metodin ulkopuolella. Luokka-attribuutteihin pääsee käsiksi mistä tahansa luokan metodista, ja ne ovat yhteisiä kaikille luokan instansseille. accumulator Muuttuja, jota käytetään silmukassa kertomaan arvosarjaa, esimerkiksi ketjuttamalla ne merkkijonoon tai lisäämällä ne juoksevaan summaan.
Tämä on vanhempi versio kirjasta, joka tunnetaan nyt nimellä Think Python. Saatat lukea mieluummin uudemman version.
How to Think Like a Computer Scientist |