Zestawy obiektów

To jest starsza wersja książki znanej obecnie jako Think Python. Być może wolisz przeczytać nowszą wersję.

How to Think Like a Computer Scientist

Rozdział 15

15.1 Kompozycja

Do tej pory widziałeś już kilka przykładów kompozycji.Jednym z pierwszych przykładów było użycie wywołania metody jako części wyrażenia. Innym przykładem jest zagnieżdżona struktura instrukcji; możesz umieścić instrukcję if wewnątrz pętli while, wewnątrz innej instrukcji if i tak dalej.

Po zapoznaniu się z tym wzorcem i poznaniu list i obiektów, nie powinieneś być zaskoczony, że możesz tworzyć listy obiektów. Możesz również tworzyć obiekty, które zawierają listy (jako atrybuty); możesz tworzyć listy, które zawierają listy; możesz tworzyć obiekty, które zawierają obiekty i tak dalej.

W tym i następnym rozdziale przyjrzymy się kilku przykładom tych kombinacji, używając obiektów Card jako przykładu.

15.2 Obiekty kart

Jeśli nie jesteś zaznajomiony ze zwykłymi kartami do gry, teraz byłby dobry moment, aby zdobyć talię, w przeciwnym razie ten rozdział może nie mieć wiele sensu.W talii są pięćdziesiąt dwie karty, z których każda należy do jednego z czterech garniturów i jednej z trzynastu rang. Kolory to Pik, Kier, Diament i Karo (w porządku malejącym w brydżu). Rangi to As, 2, 3, 4, 5,6, 7, 8, 9, 10, Walet, Dama i Król. W zależności od gry, w którą grasz, ranga Asa może być wyższa niż Króla lub niższa niż 2.

Jeśli chcemy zdefiniować nowy obiekt reprezentujący kartę do gry, oczywiste jest, jakie powinny być jego atrybuty: ranga i garnitur. Nie jest tak oczywiste, jakiego typu powinny być te atrybuty. Jedną z możliwości jest użycie łańcuchów zawierających słowa takie jak „Pik” dla koloru i „Dama” dla rangi. Jednym z problemów z tą implementacją jest to, że nie byłoby łatwo porównać karty, aby zobaczyć, która ma wyższą rangę lub kolor.

Alternatywą jest użycie liczb całkowitych do zakodowania rang i kolorów.Mówiąc „zakodować”, nie mamy na myśli tego, co niektórzy ludzie myślą, czyli zaszyfrować lub przetłumaczyć na tajny kod. To, co informatyk rozumie przez „kodowanie”, to „zdefiniowanie mapowania między ciągiem liczb a elementami, które chcę reprezentować”. Na przykład:

Pady -> 3
Szarfy -> 2
Diamenty -.> 1
Kluby -> 0

Oczywistą cechą tego odwzorowania jest to, że kombinezony odwzorowują się na liczby całkowite w kolejności, więc możemy porównywać garnitury przez porównywanie liczb całkowitych. Odwzorowanie dlaranków jest dość oczywiste; każda z liczbowych rang odpowiada odpowiadającej jej liczbie całkowitej, a dla kart z twarzą:

Jack -> 11
Królowa -> 12
Król -.> 13

Powód, dla którego używamy notacji matematycznej dla tych mapowań, jest taki, że nie są one częścią programu Pythona. Są one częścią projektu programu, ale nigdy nie pojawiają się jawnie w kodzie. Definicja klasy dla typu Card wygląda następująco:

class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank

Jak zwykle, zapewniamy metodę inicjalizacji, która przyjmuje opcjonalny parametr dla każdego atrybutu. Domyślną wartością suit jest0, co reprezentuje Clubs.

Aby utworzyć kartę, wywołujemy konstruktor Card z suit i rangą karty, którą chcemy.

threeOfClubs = Card(3, 1)

W następnej sekcji dowiemy się, jaką kartę właśnie stworzyliśmy.

15.3 Atrybuty klasy i metoda __str__

Aby wydrukować obiekty Card w sposób, który ludzie mogą łatwo odczytać, chcemy odwzorować kody liczb całkowitych na słowa. Naturalnym sposobem na zrobienie tego jest lista łańcuchów. Listy te przypisujemy do classattributes w górnej części definicji klasy:

class Card:
suitList =
rankList =
#init method omitted
def __str__(self):
return (self.rankList + ” of ” +
self.suitList)

Atrybut klasy jest zdefiniowany poza jakąkolwiek metodą i może być dostępny z dowolnej metody w klasie.

Wewnątrz __str__, możemy użyć suitList i rankList do mapowania wartości liczbowych suit i rank na łańcuchy.Na przykład, wyrażenie self.suitList oznacza „użyj atrybutu suit z obiektu self jako indeksuinto atrybutu klasy o nazwie suitList, i wybierz odpowiedni łańcuch.”

Powodem dla „narf” w pierwszym elemencie rankList jest działanie jako strażnik miejsca dla zero-etatowego elementu listy, który nigdy nie powinien być używany. Jedyne poprawne rangi to od 1 do 13. Ten zmarnowany element nie jest całkowicie konieczny. Mogliśmy zacząć od 0, jak zwykle, ale mniej mylące jest kodowanie 2 jako 2, 3 jako 3, i tak dalej.

Z pomocą metod, które mamy do tej pory, możemy tworzyć i drukować karty:

>> card1 = Card(1, 11)
>>> print card1
Jack of Diamonds

Atrybuty klasy, takie jak suitList, są wspólne dla wszystkich obiektów Card. Zaletą tego jest to, że możemy użyć dowolnego obiektu Cardobject, aby uzyskać dostęp do atrybutów klasy:

>>> card2 = Card(1, 3)
>>> print card2
3 Diamenty
>> print card2.suitList
Diamonds

Wadą tego rozwiązania jest to, że jeśli zmodyfikujemy atrybut klasy, wpłynie to na każdą jej instancję. Na przykład, jeśli zdecydujemy, że „Jack of Diamonds” powinien naprawdę nazywać się „Jack of Swirly Whales”, możemy zrobić tak:

>>> card1.suitList = „Swirly Whales”
>>> print card1
Jack of Swirly Whales

Problem polega na tym, że wszystkie karo stały się właśnie Swirly Whales:

>>> print card2
3 of Swirly Whales

Modyfikowanie atrybutów klasy zwykle nie jest dobrym pomysłem.

15.4 Porównywanie kart

Dla typów prymitywnych istnieją operatory warunkowe (<, >, ==, itd.), które porównują wartości i określają, kiedy jedna jest większa, mniejsza lub równa innej. Dla typów zdefiniowanych przez użytkownika, możemy nadpisać zachowanie wbudowanych operatorów poprzez dostarczenie metody o nazwie__cmp__. Zgodnie z konwencją, __cmp__ ma dwa parametry, self i else, i zwraca1 jeśli pierwszy obiekt jest większy, -1 jeśli drugi jest większy, i 0 jeśli są sobie równe.

Niektóre typy są całkowicie uporządkowane, co oznacza, że można porównać dowolne dwa elementy i powiedzieć, który jest większy. Na przykład, liczby całkowite i zmiennoprzecinkowe są całkowicie uporządkowane. Niektóre zbiory są nieuporządkowane, co oznacza, że nie ma sensownego sposobu, aby powiedzieć, że jeden element jest większy od drugiego. Na przykład, owoce są nieuporządkowane, dlatego nie można porównywać jabłek i pomarańczy.

Zbiór kart do gry jest częściowo uporządkowany, co oznacza, że czasami można porównywać karty, a czasami nie. Na przykład, wiesz, że trójka trefl jest wyższa niż dwójka trefl, a trójka diamentów jest wyższa niż trójka trefl. Ale która karta jest lepsza, 3 trefl czy 2 karo? Jedna ma wyższą rangę, ale druga ma wyższy kolor.

Aby karty były porównywalne, musisz zdecydować, która z nich jest ważniejsza, ranga czy kolor. Szczerze mówiąc, wybór jest arbitralny. Dla dobra wyboru, powiemy, że kolor jest ważniejszy, ponieważ nowa talia kart przychodzi posortowana ze wszystkimi treflami razem, a następnie wszystkimi karo i tak dalej.

Mając to ustalone, możemy napisać __cmp__:

def __cmp__(self, other):
# sprawdź garnitury
if self.suit > other.suit: return 1
if self.suit < other.suit: return -1
# garnitury są takie same… sprawdź rangi
if self.rank > other.rank: return 1
if self.rank < other.rank: return -1
# rangi są takie same… to jest remis
return 0

W tej kolejności asy są niżej niż dwójki (2s).

Jako ćwiczenie, zmodyfikuj __cmp__ tak, aby Asy były uporządkowane wyżej niż Króle.

15.5 Talie

Gdy mamy już obiekty do reprezentowania Kart, następnym logicznym krokiem jest zdefiniowanie klasy do reprezentowania Talii. Oczywiście, Deck składa się z kart, więc każdy obiekt Deck będzie zawierał listę kart jako atrybut.

Poniżej znajduje się definicja klasy Deck. Metoda inicjalizacyjna tworzy atrybut cards i generuje standardowy zestaw pięćdziesięciu dwóch kart:

class Deck:
def __init__(self):
self.cards =
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))

Najprostszym sposobem na zaludnienie talii jest pętla zagnieżdżona. Pętla zewnętrzna wylicza kolory od 0 do 3, natomiast pętla wewnętrzna wylicza rangi od 1 do 13. Ponieważ pętla zewnętrzna iteruje cztery razy, a pętla wewnętrzna trzynaście razy, całkowita liczba wykonań ciała wynosi pięćdziesiąt dwa (trzynaście razy cztery). Każda iteracja tworzy nową instancję karty Card z bieżącym kolorem i rangą oraz dołącza tę kartę do listy kart.

Metoda append działa na listach, ale oczywiście nie na tuplach.

15.6 Drukowanie talii

Jak zwykle, gdy definiujemy nowy typ obiektu, chcemy mieć metodę, która wypisuje zawartość obiektu.Aby wydrukować talię, przeglądamy listę i drukujemy każdą kartę:

class Deck:

def printDeck(self):
for card in self.cards:
print card

Tutaj, i od teraz, elipsa (…) wskazuje, że pominęliśmy inne metody w klasie.

Jako alternatywę dla printDeck, moglibyśmy napisać metodę __str__ dla klasy Deck. Zaletą metody __str__ jest to, że jest bardziej elastyczna. Zamiast po prostu drukować zawartość obiektu, generuje ona reprezentację łańcuchową, którą inne części programu mogą manipulować przed wydrukiem lub przechowywać do późniejszego wykorzystania.

Oto wersja metody __str__, która zwraca reprezentację łańcuchową klasy Deck.Aby dodać trochę pizzazz, układa karty w kaskadę, gdzie każda karta jest wcięta o jedną spację więcej niż poprzednia:

class Deck:
….
def __str__(self):
s = „”
for i in range(len(self.cards)):
s = s + „”*i + str(self.cards) + „”
return s

Ten przykład demonstruje kilka cech. Po pierwsze, zamiast przemierzać self.cards i przypisywać każdą kartę do zmiennej, używamy i jako zmiennej pętli i indeksu do listy kart.

Po drugie, używamy operatora mnożenia łańcuchów do indentacji każdej karty o jedną spację więcej niż ostatnia. Wyrażenie””*i daje liczbę spacji równą bieżącej wartości i.

Po trzecie, zamiast używać polecenia print do drukowania kart, używamy funkcji str. Przekazanie obiektu jako argumentu dostr jest równoważne wywołaniu metody __str__ na tym obiekcie.

Na koniec, używamy zmiennej s jako akumulatora.Początkowo s jest pustym łańcuchem. Za każdym razem w pętli generowany jest nowy łańcuch i konkatenowany ze starą wartością sto, aby uzyskać nową wartość. Gdy pętla się kończy, s zawiera kompletną reprezentację łańcucha Deck, która wygląda tak:

>>> deck = Deck()
>>> print deck
Ace of Clubs
2 of Clubs
3 of Clubs
4 of Clubs
5 of Clubs
6 of trefl
7 trefl
8 trefl
9 trefl
10 trefl
Jack trefl
Król trefl
Król karo

I tak dalej. Mimo że wynik znajduje się w 52 wierszach, jest to jeden długi łańcuch zawierający nowe linie.

15.7 Tasowanie talii

Jeśli talia jest idealnie potasowana, to każda karta ma takie samo prawdopodobieństwo pojawienia się w dowolnym miejscu w talii, a każde miejsce w talii ma takie samo prawdopodobieństwo znalezienia się w nim dowolnej karty.

Aby potasować talię, użyjemy funkcji randrange z modułu random. Z dwoma argumentami całkowitymi, a i b, randrange wybiera losową liczbę całkowitą z zakresu a <= x < b. Ponieważ górna granica jest ściśle mniejsza od b, możemy użyć długości listy jako drugiego argumentu i mamy gwarancję, że otrzymamy legalny indeks.Na przykład, to wyrażenie wybiera indeks losowej karty w talii:

random.randrange(0, len(self.cards))

Łatwym sposobem na potasowanie talii jest przejście przez karty i zamiana każdej z nich na losowo wybraną. Jest możliwe, że karta zostanie zamieniona z samą sobą, ale to jest w porządku. W rzeczywistości, gdybyśmy wykluczyli taką możliwość, kolejność kart byłaby mniej niż całkowicie losowa:

class 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

Zamiast zakładać, że w talii są pięćdziesiąt dwie karty, pobieramy rzeczywistą długość listy i zapisujemy ją w nCards.

Dla każdej karty w talii wybieramy losową kartę spośród kart, które nie zostały jeszcze potasowane. Następnie zamieniamy aktualną kartę (i) z wybraną kartą (j). Do zamiany kart używamy przypisania tuple, jak w rozdziale 9.2:

self.cards, self.cards = self.cards, self.cards

Jako ćwiczenie, przepisz ten wiersz kodu bez użycia przypisania sekwencji.

15.8 Usuwanie i rozdawanie kart

Inną metodą, która przydałaby się w klasie Deck, jest removeCard, która przyjmuje kartę jako argument, usuwa ją i zwraca True, jeśli karta była w talii i False else:

class Deck:

def removeCard(self, card):
if card in self.cards:
self.cards.remove(card)
return True
else:
return False

Operator in zwraca true, jeśli pierwszy operand znajduje się w thesecond, które musi być listą lub tuple. Jeśli pierwszy operand jest obiektem, Python używa metody __cmp__ tego obiektu do określenia nierówności z elementami listy. Ponieważ __cmp__ w klasieCard sprawdza głęboką równość, metoda removeCard sprawdza głęboką równość.

Aby rozdać karty, chcemy usunąć i zwrócić wierzchnią kartę.Metoda listy pop zapewnia wygodny sposób, aby to zrobić:

class Deck:

def popCard(self):
return self.cards.pop()

Właściwie pop usuwa ostatnią kartę na liście, więc w efekcie rozdajemy karty od dołu talii.

Jeszcze jedna operacja, która prawdopodobnie będzie nam potrzebna, to funkcja booleanisEmpty, która zwraca true, jeśli talia nie zawiera kart:

class Deck:

def isEmpty(self):
return (len(self.cards) == 0)

15.9 Glosariusz

kodowanie Reprezentowanie jednego zestawu wartości za pomocą innego zestawu wartości poprzez skonstruowanie mapowania między nimi. atrybut klasy Zmienna, która jest zdefiniowana wewnątrz definicji klasy, ale poza jakąkolwiek metodą. Atrybuty klasy są dostępne z każdej metody w klasie i są współdzielone przez wszystkie instancje klasy. accumulator Zmienna używana w pętli do akumulowania serii wartości, np. przez konkatenację ich w łańcuch lub dodawanie do sumy bieżącej.

Jest to starsza wersja książki znanej obecnie jako Think Python. Być może wolisz przeczytać nowszą wersję.

How to Think Like a Computer Scientist

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.