Think Python/類和物件
使用者定義型別
[edit | edit source]我們已經使用了 Python 的許多內建型別;現在我們將定義一個新的型別。舉個例子,我們將建立一個名為Point的型別,它代表二維空間中的一個點。
在數學符號中,點通常用括號括起來,用逗號分隔座標。例如,(0, 0) 代表原點,(x, y) 代表從原點向右移動 x 個單位,向上移動 y 個單位的點。
在 Python 中,我們可以用幾種方式表示點
- 我們可以將座標分別儲存在兩個變數中:x和y.
- 我們可以將座標儲存為列表或元組中的元素。
- 我們可以建立一個新型別來表示點作為物件。
建立新型別比其他選項(稍微)更復雜,但它有一些很快就會顯現的優勢。
使用者定義型別也稱為類。類定義看起來像這樣
class Point(object):
"""represents a point in 2-D space"""
此標題表示新類是一種Point,它是object的一種,而它是內建型別。
主體是一個文件字串,它解釋了類的用途。你可以在類定義中定義變數和函式,但我們稍後會回到這一點。
定義名為Point的類會建立一個類物件。
>>> print Point
<class '__main__.Point'>
因為Point是在頂層定義的,所以它的“全名”是 __main__.Point。
類物件就像一個用於建立物件的工廠。要建立一個 Point,你需要呼叫Point,就像它是一個函式一樣。
>>> blank = Point()
>>> print blank
<__main__.Point instance at 0xb7e9d3ac>
返回值是對 Point 物件的引用,我們將它分配給空白。建立新物件稱為例項化,而該物件是類的例項。
當你列印一個例項時,Python 會告訴你它屬於哪個類,以及它儲存在記憶體中的位置(字首0x表示後面的數字是十六進位制的)。
屬性
[edit | edit source]你可以使用點表示法為例項分配值
>>> blank.x = 3.0
>>> blank.y = 4.0
此語法類似於從模組中選擇變數的語法,例如math.pi或string.whitespace。但是,在這種情況下,我們正在將值分配給物件的命名元素。這些元素稱為屬性。
作為名詞,“AT-trib-ute”的重音在第一個音節上,而“a-TRIB-ute”是動詞。
下圖顯示了這些賦值的結果。顯示物件及其屬性的狀態圖稱為物件圖
變數空白引用一個 Point 物件,該物件包含兩個屬性。每個屬性都引用一個浮點數。
你可以使用相同的語法讀取屬性的值
>>> print blank.y
4.0
>>> x = blank.x
>>> print x
3.0
表示式blank.x的意思是,“轉到物件空白所引用的位置,並獲取x的值”。在這種情況下,我們將該值分配給一個名為x的變數。變數x和屬性x.
之間不存在衝突。你可以在任何表示式中使用點表示法。例如
>>> print '(%g, %g)' % (blank.x, blank.y)
(3.0, 4.0)
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> print distance
5.0
你可以按常規方式將例項作為引數傳遞。例如
def print_point(p):
print '(%g, %g)' % (p.x, p.y)
print_point 接受一個點作為引數,並以數學符號顯示它。要呼叫它,你可以傳遞空白作為引數
>>> print_point(blank)
(3.0, 4.0)
在函式內部,p是空白的別名,因此如果函式修改了p, 空白,則會發生改變。
練習 1
[edit | edit source]編寫一個名為“distance”的函式,它接受兩個 Point 作為引數,並返回它們之間的距離。
矩形
[edit | edit source]有時,物件的屬性是顯而易見的,但有時你需要做出決定。例如,想象你正在設計一個類來表示矩形。你會使用哪些屬性來指定矩形的位置和大小?你可以忽略角度;為了簡化起見,假設矩形是垂直或水平的。
至少有兩種可能性
- 你可以指定矩形的一個角(或中心)、寬度和高度。
- 你可以指定兩個相對的角。
在這一點上,很難說哪一個比另一個更好,所以我們將實現第一個,僅僅作為示例。
這是類定義
class Rectangle(object):
"""represent a rectangle.
attributes: width, height, corner.
"""
文件字串列出了屬性width和height是數字;corner是一個 Point 物件,它指定了左下角。
要表示一個矩形,你需要例項化一個 Rectangle 物件,併為屬性分配值
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的屬性。”
該圖顯示了該物件的狀態
File:Book023.png 作為另一個物件屬性的物件是嵌入的。
例項作為返回值
[edit | edit source]函式可以返回例項。例如,find_center 接受一個Rectangle作為引數,並返回一個Point,其中包含Rectangle:
def find_center(box):
p = Point()
p.x = box.corner.x + box.width/2.0
p.y = box.corner.y + box.height/2.0
return p
中心的座標。以下是一個將box作為引數傳遞並將生成的 Point 分配給center:
>>> center = find_center(box)
>>> print_point(center)
(50.0, 100.0)
物件的例子。
物件是可變的[edit | edit source]width和height:
box.width = box.width + 50
box.height = box.height + 100
你可以透過對物件的某個屬性進行賦值來改變物件的狀態。例如,要改變矩形的大小而不改變其位置,你可以修改的值。你也可以編寫修改物件的函式。例如,grow_rectangle 接受一個 Rectangle 物件和兩個數字,和dwidthdheight
def grow_rectangle(rect, dwidth, dheight) :
rect.width += dwidth
rect.height += dheight
,並將這些數字新增到矩形的寬度和高度
>>> print box.width
100.0
>>> print box.height
200.0
>>> grow_rectangle(box, 50, 100)
>>> print box.width
150.0
>>> print box.height
300.0
在函式內部,以下是一個演示效果的例子是box的別名,因此如果函式修改了以下是一個演示效果的例子, box,則會發生改變。
rect
練習 2[edit | edit source]編寫一個名為 move_rectangle 的函式,它接受一個 Rectangle 和兩個名為和dxdy編寫一個名為 move_rectangle 的函式,它接受一個 Rectangle 和兩個名為的數字。它應該透過將x新增到corner的座標和將dx的數字。它應該透過將y新增到corner.
新增到
的座標來改變矩形的位置。複製
[edit | edit source]別名會使程式難以閱讀,因為在一個地方的更改可能會在另一個地方產生意想不到的影響。很難跟蹤所有可能引用給定物件的變數。複製物件通常是別名的替代方案。該別名會使程式難以閱讀,因為在一個地方的更改可能會在另一個地方產生意想不到的影響。很難跟蹤所有可能引用給定物件的變數。copy
>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0
>>> import copy
>>> p2 = copy.copy(p1)
模組包含一個名為和的函式,它可以複製任何物件p1
>>> print_point(p1)
(3.0, 4.0)
>>> print_point(p2)
(3.0, 4.0)
>>> p1 is p2
False
>>> p1 == p2
False
p2包含相同的資料,但它們不是同一個 Point。該模組包含一個名為和的函式,它可以複製任何物件運算子表示==不是同一個物件,這正如我們所料。但你可能期望產生True==,因為這些點包含相同的資料。在這種情況下,你會很失望地發現,對於例項,該包含相同的資料,但它們不是同一個 Point。運算子的預設行為與該
運算子相同;它檢查物件標識,而不是物件等效性。此行為可以更改——我們稍後會看到如何更改。如果你使用要複製一個矩形,你會發現它複製了矩形物件,但沒有複製嵌入的點物件。
>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True
以下是物件圖的樣子:
此操作稱為淺複製,因為它複製了物件及其包含的任何引用,但不復制嵌入的物件。
對於大多數應用程式來說,這不是你想要的。在這個例子中,對其中一個矩形呼叫grow_rectangle不會影響另一個矩形,但是對任何一個矩形呼叫move_rectangle都會影響兩個矩形!這種行為令人困惑且容易出錯。
幸運的是,別名會使程式難以閱讀,因為在一個地方的更改可能會在另一個地方產生意想不到的影響。很難跟蹤所有可能引用給定物件的變數。模組包含一個名為deepcopy的方法,它不僅複製了物件,還複製了它引用的物件,以及它們引用的物件,等等。你不會驚訝地發現這個操作被稱為深複製。
>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
box3和box是完全獨立的物件。
編寫一個move_rectangle版本,它建立並返回一個新的矩形,而不是修改舊的矩形。
當你開始使用物件時,你可能會遇到一些新的異常。如果你嘗試訪問不存在的屬性,你會得到一個AttributeError:
>>> p = Point()
>>> print p.z
AttributeError: Point instance has no attribute 'z'
如果你不確定物件的型別,你可以詢問
>>> type(p)
<type '__main__.Point'>
如果你不確定物件是否具有特定屬性,可以使用內建函式hasattr:
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False
第一個引數可以是任何物件;第二個引數是一個字串,包含屬性的名稱。
- class
- 使用者定義的型別。類定義建立新的類物件。
- 類物件
- 包含使用者定義型別資訊的
物件。類物件可以用來建立該型別的例項。 - 例項
- 屬於某個類的物件。
- 屬性
- 與物件關聯的命名值之一。
- 嵌入(物件)
- 作為另一個物件屬性儲存的物件。
- 淺複製
- 複製物件的
內容,包括對嵌入物件的任何引用;由別名會使程式難以閱讀,因為在一個地方的更改可能會在另一個地方產生意想不到的影響。很難跟蹤所有可能引用給定物件的變數。函式在別名會使程式難以閱讀,因為在一個地方的更改可能會在另一個地方產生意想不到的影響。很難跟蹤所有可能引用給定物件的變數。模組中實現。 - 深複製
- 複製物件的
內容以及任何嵌入的物件,以及它們嵌入的任何物件,等等;由deepcopy函式在別名會使程式難以閱讀,因為在一個地方的更改可能會在另一個地方產生意想不到的影響。很難跟蹤所有可能引用給定物件的變數。模組中實現。 - 物件圖
- 一個圖,它顯示了物件、它們的屬性和屬性的值。
World.py,它是 Swampy 的一部分(見第 '4' 章),包含一個用於稱為 World 的使用者定義型別的類定義。如果你執行這段程式碼: from World import * world = World() wait_for_user() 應該會彈出一個帶標題欄和一個空正方形的視窗。在本練習中,我們將使用此視窗繪製點、矩形和其他形狀。在wait_for_user之前新增以下幾行,然後再次執行程式
''canvas = world.ca(width=500, height=500, background='white')
bbox = [[-150,-100], [150, 100]]
canvas.rectangle(bbox, outline='black', width=2, fill='green4')
你應該看到一個帶有黑色邊框的綠色矩形。第一行建立了一個 Canvas,它在視窗中顯示為一個白色正方形。Canvas 物件提供了像rectangle這樣的方法,用於繪製各種形狀。
bbox是一個列表的列表,它表示矩形的“邊界框”。第一對座標是矩形的左下角;第二對座標是右上角。
你可以這樣繪製一個圓形
canvas.circle([-25,0], 70, outline=None, fill='red')
第一個引數是圓心座標對;第二個引數是半徑。
如果你在程式中新增這行程式碼,結果應該類似於孟加拉國國旗(見wikipedia.org/wiki/Gallery_of_sovereign-state_flags).
- 編寫一個名為
draw_rectangle的函式,它接收一個 Canvas 和一個矩形作為引數,並在 Canvas 上繪製矩形的表示。
- 在你的矩形物件中新增一個名為color的屬性,並修改
draw_rectangle,以便它使用 color 屬性作為填充顏色。
- 編寫一個名為
draw_point的函式,它接收一個 Canvas 和一個點作為引數,並在 Canvas 上繪製點的表示。
- 定義一個名為 Circle 的新類,它具有適當的屬性,並例項化幾個 Circle 物件。編寫一個名為
draw_circle的函式,它在畫布上繪製圓形。
- 編寫一個繪製捷克共和國國旗的程式。提示:你可以這樣繪製多邊形
points = [[-150,-100], [150, 100], [150, -100]]
canvas.polygon(points, fill='blue')
我寫了一個小程式,它列出了可用的顏色;你可以從thinkpython.com/code/color_list.py.