# 第十五章 類和對象
到目前為止,你應該已經知道如何用函數來整理代碼,以及用內置類型來組織數據了。接下來的一步就是要學習『面向對象編程』了,這種編程方法中,用戶可以自定義類型來同時對代碼和數據進行整理。面向對象編程是一個很大的題目;要有好幾章才能講出個大概。
本章的樣例代碼可以在[這里](http://thinkpython2.com/code/Point1.py)來下載,練習題對應的樣例代碼可以在[這里](http://thinkpython2.com/code/Point1_soln.py)下載。
## 15.1 用戶自定義類型
我們已經用過很多 Python 的內置類型了;現在我們就要來定義一個新的類型了。作為樣例,我們會創建一個叫 Point 的類,用于表示一個二維空間中的點。
數學符號上對點的表述一般是一個括號內有兩個坐標,坐標用逗號分隔開。比如,(0,0)就表示為原點,(x,y)就表示了該點從原點向右偏移 x,向上偏移 y。
我們可以用好幾種方法來在 Python 中表示一個點:
? 我們可以把坐標存儲成兩個單獨的值,x 和 y。
? 還可以把坐標存儲成列表或者元組的元素。
? 還可以創建一個新的類型來用對象表示點。
創建新的類型要比其他方法更復雜一點,不過也有一些優勢,等會我們就會發現了。
用戶自定義的類型也被叫做一個類。一個類的定義大概是如下所示的樣子:
```py
class Point:
"""Represents a point in 2-D space."""
```
頭部代碼的意思是表示新建的類名字叫 Point。然后類的體內有一個文檔字符串,解釋類的用途。在類的定義內部可以定義各種變量和方法,等會再來詳細學習一下這些內容哈。
聲明一個名為 Point 的類,就可以創建該類的一個對象。
```py
>>> Point
<class '__main__.Point'>
```
因為 Point 是在頂層位置定義的,所以全名就是 __main__.Point。
類的對象就像是一個創建對象的工廠。要創建一個 Point,就可以像調用函數一樣調用 Point。
```py
>>> blank = Point()
>>> blank
<__main__.Point object at 0xb7e9d3ac>
```
返回值是到一個 Point 對象的引用,剛剛賦值為空白了。
創建一個新的對象也叫做實例化,這個對象就是類的一個實例。
用 Print 輸出一個實例的時候,Python 會告訴你該實例所屬的類,以及在內存中存儲的位置(前綴為 0x 意味著下面的這些數值是十六進制的。)
每一個對象都是某一個類的一個實例,所以『對象』和『實例』可以互換來使用。不過本章我還是都使用『實例』這個詞,這樣就更能體現出咱們在談論的是用戶定義的類型。
## 15.2 屬性
用點號可以給實例進行賦值:
```py
>>> blank.x = 3.0
>>> blank.y = 4.0
```
這一語法形式就和從模塊中選取變量的語法是相似的,比如 math.pi 或者 string.whitespace。然而在本章這種情況下,我們用點號實現的是對一個對象中某些特定名稱的元素進行賦值。這些元素也叫做屬性。
『Attribute』作為名詞的發音要把重音放在第一個音節,而做動詞的時候是重音放到第二音節。
下面的圖表展示了上面這些賦值的結果。用于展示一個類及其屬性的狀態圖也叫做類圖;比如圖 15.1 就是一例。
________________________________________

Figure 15.1: Object diagram.
________________________________________
變量 blank 指代的是一個 Point 對象,該對象包含兩個屬性。每個屬性都指代了一個浮點數。
讀取屬性值可以用如下這樣的語法:
```py
>>> blank.y
4.0
>>> x = blank.x
>>> x
3.0
```
這里的表達式 blank.x 的意思是,『到 blank 所指代的對象中,讀取 x 的值。』在這個例子中,我們把這個值賦值給一個名為 x 的變量。這里的變量 x 和類的屬性 x 并不沖突。
點號可以隨意在任意表達式中使用。比如下面這個例子:
```py
>>> '(%g, %g)' % (blank.x, blank.y)
'(3.0, 4.0)'
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> distance
5.0
```
你還可以把實例作為一個參數來使用。比如下面這樣:
```py
def print_point(p):
print('(%g, %g)' % (p.x, p.y))
```
print_point 這個函數就接收了一個點作為參數,然后顯示點的數值位置。你可以把剛剛那個 blank 作為參數傳過去來試試:
```py
>>> print_point(blank)
(3.0, 4.0)
```
在函數內部,p 是 blank 的一個別名,所以如果函數內部對 p 進行了修改,blank 也會發生相應的改變。
做個練習,寫一個名為 distance_between_points 的函數,接收兩個點作為參數,然后返回兩點之間的距離。
## 15.3 矩形
有時候一個類中的屬性應該如何設置是很明顯的,不過有的時候就得好好考慮一下了。比如,假設你要設計一個表示矩形的類。你要用什么樣的屬性來確定一個矩形的位置和大小呢?可以忽略角度;來讓情況更簡單一些,就只考慮矩形是橫向的或者縱向的。
至少有兩種方案備選:
? 確定矩形的一個頂點(或者中心)所在位置,還有寬度和高度。
? 確定對角線上的兩個頂點所在位置。
現在還很難說這兩者哪一個更好,那么咱們先用第一個方案來做個例子。
下面就是類的定義:
```py
class Rectangle:
"""Represents a rectangle.
attributes: width, height, corner.
"""
```
文檔字符串中列出了屬性:width 和 height 是數值;corner 是一個點對象,用來表示左下角頂點。
要表示一個矩形,必須初始化一個矩形對象,然后對其屬性進行賦值:
```py
box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0
```
表達式 box.corner.x 的意思是,『到 box 指代的對象中,選擇名為 corner 的屬性;然后到這個點對象中,選取名為 x 的屬性值。』
________________________________________

Figure 15.2: Object diagram.
________________________________________
圖 15.2 展示了這個對象的狀態圖。一個類去作為另外一個類的屬性,就叫做嵌入。
## 15.4 多個實例作返回值
函數返回實例。比如 find_center 就接收一個 Rectangle (矩陣)對象作為參數,然后以一個 Point(點)對象的形式返回矩形中心位置的坐標所在點:
```py
def find_center(rect):
p = Point()
p.x = rect.corner.x + rect.width/2
p.y = rect.corner.y + rect.height/2
return p
```
下面這個例子中,box 作為一個參數傳遞給了 find_center 函數,然后結果賦值給了點 center:
```py
>>> center = find_center(box)
>>> print_point(center)
(50, 100)
```
## 15.5 對象可以修改
通過對一個對象的屬性進行賦值就可以修改該對象的狀態了。比如,要改變一個舉行的大小而不改變位置,就可以只修改寬度和高度,如下所示:
```py
box.width = box.width + 50
box.height = box.height + 100
```
你還可以寫專門的函數來修改對象。比如 grow_rectangle 這個函數就接收一個矩形對象和 dwidth 與 dheight 兩個數值,然后把這兩個數值加到矩形的寬度和高度值上。
```py
def grow_rectangle(rect, dwidth, dheight):
rect.width += dwidth
rect.height += dheight
```
下面的例子展示了具體的效果:
```py
>>> box.width, box.height
(150.0, 300.0)
>>> grow_rectangle(box, 50, 100)
>>> box.width, box.height
(200.0, 400.0)
```
在函數的內部,rect 是 box 的一個別名,所以當函數修改了 rect 的時候,box 就得到了相應的修改。
做個練習,寫一個名為 move_rectangle 的函數,接收一個矩形和 dx 與 dy 兩個數值。函數要改變矩形所在位置,具體的改變方法為對左下角頂點坐標的 x 和 y 分別加上 dx 和 dy 的值。
## 15.6 復制
別名有可能讓程序讀起來有困難,因為在一個位置做出的修改有可能導致另外一個位置發生不可預知的情況。這樣也很難去追蹤指向一個對象的所有變量。
所以就可以不用別名,而用復制對象的方法。copy 模塊包含了一個名叫 copy 的函數,可以復制任意對象:
```py
>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0
>>> import copy
>>> p2 = copy.copy(p1)
```
p1 和 p2 包含的數據是相同的,但并不是同一個點對象。
```py
>>> print_point(p1)
(3, 4)
>>> print_point(p2)
(3, 4)
>>> p1 is p2
False
>>> p1 == p2
False
```
is 運算符表明 p1 和 p2 不是同一個對象,這就是我們所預料的。但你可能本想著是==運算符應該得到的是 True 因為這兩個點包含的數據是一樣的。這樣的話你就會很失望地發現對于實例來說,==運算符的默認行為就跟 is 運算符是一樣的;它也還是檢查對象的身份,而不是對象的相等性。這是因為你用的是用戶自定義的類型,Python 不值得如何去衡量是否相等。至少是現在還不能。
(譯者注:==運算符的實現需要運算符重載,也就是多態的一種,來實現,也就是對用戶自定義類型,需要用戶自定義運算符,而不能簡單地繼續用內置運算符。因為自定義類型的運算是 Python 沒法確定的,得用戶自己來確定。)
如果你用 copy.copy 復制了一個矩形,你會發現該函數復制了矩形對象,但沒有復制內嵌的點對象。
```py
>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True
```
________________________________________

Figure 15.3: Object diagram.
________________________________________
圖 15.3 展示了此時的類圖的情況。這種運算叫做淺復制,因為復制了對象與對象內包含的所有引用,但不復制內嵌的對象。
對于大多數應用來說,這并不是你的本來目的。在本節的樣例中,對復制過的一個矩形進行 grow_rrectangle 函數運算,并不會影響另外一個,但使用 move_rectangle 就會對兩個都有影響!這種行為就很讓人疑惑,也容易帶來錯誤。
所幸的是 copy 模塊還提供了一個名為 deepcopy (深復制)的方法,這樣就能把內嵌的對象也復制了。你肯定不會奇怪了,這種運算就叫深復制了。
```py
>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
```
box3 和 box 就是完全隔絕開,沒有公用內嵌對象,徹底不會相互干擾的兩個對象了。
做個練習吧,寫一個新版本的 move_rectangle,創建并返回一個新的矩形,而不是修改舊有的矩形。
## 15.7 調試
當你開始使用對象的時候,你就容易遇到一些新的異常。如果你試圖讀取一個不存在的屬性,就會得到一個屬性錯誤 AttributeError:
```py
>>> p = Point()
>>> p.x = 3
>>> p.y = 4
>>> p.z
AttributeError: Point instance has no attribute 'z'
```
如果不確定一個對象是什么類型,可以『問』一下:
```py
>>> type(p)
<class '__main__.Point'>
```
還可以用 isinstance 函數來檢查一下一個對象是否為某一個類的實例:
```py
>>> isinstance(p, Point)
True
```
如果不確定某一對象是否有一個特定的屬性,可以用內置函數 hasattr:
```py
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False
```
hasattr 的第一個參數可以是任意一個對象;第二個參數是一個字符串,就是要判斷是否存在的屬性名字。
用 try 語句也可以試驗一個對象是否有你需要的屬性:
```py
try:
x = p.x
except AttributeError:
x = 0
```
這樣寫一些處理不同類型變量的函數就更容易了;關于這一話題的更多內容會在 17.9 中展開。
## 15.8 Glossary 術語列表
class:
A programmer-defined type. A class definition creates a new class object.
>類:用戶定義的類型。一個類的聲明建立了一個新的類的對象。
class object:
An object that contains information about a programmer-defined type. The class object can be used to create instances of the type.
>類的對象:包含了用戶自定義類型相關信息的一個對象。可以用于創建類的一個實例。
instance:
An object that belongs to a class.
>實例:術語某一個類的一個對象。
instantiate:
To create a new object.
>實例化:創建一個新的對象。
attribute:
One of the named values associated with an object.
>屬性:一個對象內附屬的數值的名字。
embedded object:
An object that is stored as an attribute of another object.
>內嵌對象:一個對象作為屬性存儲在另一個對象內。
shallow copy:
To copy the contents of an object, including any references to embedded objects; implemented by the copy function in the copy module.
>淺復制:復制一個對象中除了內嵌對象之外的所有引用;通過 copy 模塊的 copy 函數來實現。
deep copy:
To copy the contents of an object as well as any embedded objects, and any objects embedded in them, and so on; implemented by the deepcopy function in the copy module.
>深復制:復制一個對象的所有內容,包括內嵌對象,以及內嵌對象中的所有內嵌對象等等;通過 copy 模塊的 deepcopy 函數來實現。
object diagram:
A diagram that shows objects, their attributes, and the values of the attributes.
>類圖:一種圖解,用于展示類與類中的屬性以及屬性的值。
## 15.9 練習
### 練習 1
寫一個名為 Circle 的類的定義,屬性為圓心 center 和半徑 radius,center 是一個點對象,半徑是一個數值。
實例化一個 Circle 的對象,表示一個圓,圓心在(150,100),半徑為 75。
寫一個名為 point_in_circle 的函數,接收一個 Circle 和一個 Point 對象作為參數,如果點在圓內或者圓的線上就返回 True。
寫一個名為 rect_in_circle 的函數,接收一個 Circle 和一個 Rectangle 對象作為參數,如果矩形的邊都內含或者內切在圓內,就返回 True。
寫一個名為 rect_circle_overlap 的函數,接收一個 Circle 和一個 Rectangle 對象作為參數,如果矩形任意一個頂點在圓內就返回 True。或者寫個更有挑戰性的版本,如果矩形有任意部分包含在圓內就返回 True。
[樣例代碼](http://thinkpython2.com/code/Circle.py)。
### 練習 2
寫一個名為 draw_rect 的函數,接收一個 Turtle 對象和一個 Rectangle 對象作為參數,用 Turtle 畫出這個矩形。可以參考一下第四章對 Turtle 對象使用的樣例。
寫一個名為 draw_circle 的函數,接收一個 Turtle 對象和一個 Circle 對象,畫個圓這次。
[樣例代碼](http://thinkpython2.com/code/draw.py)。