Think Python/類和方法
面向物件特性
[edit | edit source]Python 是一種面向物件程式語言,這意味著它提供了支援面向物件程式設計的功能。
面向物件程式設計不容易定義,但我們已經看到了一些它的特性。
- 程式由物件定義和函式定義組成,大多數計算都用物件上的操作來表達。
- 每個物件定義對應於現實世界中的一些物件或概念,而作用於該物件上的函式對應於現實世界物件互動的方式。
例如,時間第 16 章中定義的類對應於人們記錄一天中時間的做法,我們定義的函式對應於人們對時間進行的操作型別。類似地,點和矩形類對應於點和矩形的數學概念。
到目前為止,我們還沒有利用 Python 提供的支援面向物件程式設計的功能。這些功能並非嚴格必要;它們中的大多數為我們已經做過的事情提供了替代語法。但在許多情況下,替代方法更簡潔,更準確地傳達了程式的結構。
例如,在時間程式中,類定義和隨後的函式定義之間沒有明顯的聯絡。經過一些檢查,很明顯每個函式至少接受一個時間物件作為引數。
這個觀察結果是方法的動機;方法是與特定類相關聯的函式。我們已經看到了字串、列表、字典和元組的方法。在本節中,我們將為使用者定義型別定義方法。
方法在語義上與函式相同,但有兩個語法差異
- 方法在類定義內定義,以使類和方法之間的關係顯式。
- 呼叫方法的語法不同於呼叫函式的語法。
在接下來的幾節中,我們將從前兩節中獲取函式,並將它們轉換為方法。這種轉換純粹是機械的;你只需按照一系列步驟即可完成。如果你習慣於從一種形式轉換到另一種形式,你將能夠為你的任何操作選擇最佳形式。
列印物件
[edit | edit source]在第 16 章中,我們定義了一個名為時間的類,在練習 16.1 中,你編寫了一個名為 print_time 的函式
class Time(object):
"""represents the time of day.
attributes: hour, minute, second"""
def print_time(time):
print '%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)
要呼叫此函式,你必須傳遞一個時間物件作為引數
>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 00
>>> print_time(start)
09:45:00
要使 print_time 成為方法,我們只需將函式定義移動到類定義內。請注意縮排的改變。
class Time(object):
def print_time(time):
print '%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)
現在有兩種方法可以呼叫 print_time。第一種(也是不太常見的一種)方法是使用函式語法
>>> Time.print_time(start)
09:45:00
在此使用點表示法中,時間是類的名稱,print_time 是方法的名稱。開始作為引數傳遞。
第二種(也是更簡潔的)方法是使用方法語法
>>> start.print_time()
09:45:00
在此使用點表示法中,print_time 是方法的名稱(同樣),並且開始是呼叫方法的物件,稱為主體。就像句子的主體是句子所指的物件一樣,方法呼叫的主體是方法所指的物件。
在方法內部,主體被分配給第一個引數,因此在這種情況下開始被分配給時間.
按照慣例,方法的第一個引數稱為自己,因此更常見的是這樣寫 print_time
class Time(object):
def print_time(self):
print '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
這樣做的原因是隱含的隱喻
- 函式呼叫的語法,
print_time(start),
表明函式是活動代理。它類似於說,“嘿 print_time!這裡有一個你要列印的物件。”
- 在面向物件程式設計中,物件是活動代理。
類似於 start.print_time() 這樣的方法呼叫表示“嘿開始!請列印你自己。”
這種視角的改變可能更加禮貌,但它並非明顯有用。在我們目前看到的示例中,它可能不是。但有時將責任從函式轉移到物件可以使編寫更通用的函式成為可能,並且使程式碼更易於維護和重用。
練習 1
[edit | edit source]將 time_to_int (來自第 '16.4' 節)改寫為方法。改寫 int_to_time 為方法可能不合適;不清楚你要在哪個物件上呼叫它!
另一個例子
[edit | edit source]以下是增加(來自第 16.3 節)改寫為方法的版本
# inside class Time:
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
此版本假設 time_to_int 作為方法編寫,如練習 17.1 中所示。此外,請注意它是一個純函式,而不是一個修飾符。
以下是呼叫增加:
>>> start.print_time()
09:45:00
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17
主體,開始,被分配給第一個引數,自己。引數,1337,被分配給第二個引數,秒.
此機制可能會令人困惑,尤其是在你犯錯的情況下。例如,如果你呼叫增加帶有兩個引數,你會得到
>>> end = start.increment(1337, 460)
TypeError: increment() takes exactly 2 arguments (3 given)
錯誤訊息最初令人困惑,因為括號中只有兩個引數。但主體也被認為是一個引數,因此總共是三個。
更復雜的例子
[edit | edit source]is_after(來自練習 16.2)稍微複雜一些,因為它接受兩個 Time 物件作為引數。在這種情況下,通常將第一個引數命名為自己,第二個引數命名為其他:
# inside class Time:
def is_after(self, other):
return self.time_to_int() > other.time_to_int()
要使用此方法,你必須在一個物件上呼叫它,並將另一個物件作為引數傳遞
>>> end.is_after(start)
True
這種語法的一個優點是它幾乎像英語一樣易懂:“結束在開始之後嗎?”
init 方法
[edit | edit source]init 方法(初始化的縮寫)是一個特殊方法,在例項化物件時被呼叫。它的全名是 __init__(兩個下劃線字元,後跟初始化,然後是另外兩個下劃線)。的 init 方法時間類可能如下所示
# inside class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
__init__ 的引數通常與屬性具有相同的名稱。語句
self.hour = hour
將引數的值小時儲存為的屬性自己.
引數是可選的,因此如果你呼叫時間不帶任何引數,你將獲得預設值。
>>> time = Time()
>>> time.print_time()
00:00:00
如果你提供一個引數,它將覆蓋小時:
>>> time = Time (9)
>>> time.print_time()
09:00:00
如果你提供兩個引數,它們將覆蓋小時和分鐘.
>>> time = Time(9, 45)
>>> time.print_time()
09:45:00
如果你提供三個引數,它們將覆蓋所有三個預設值。
練習 2
[edit | edit source]為 Point 類編寫一個 init 方法,該方法接受 x 和 y 作為可選引數,並將它們分配給相應的屬性。
這__str__方法
[edit | edit source]__str__ 是一種特殊方法,類似於 __init__,用於返回物件的字串表示形式。
例如,這裡有一個str用於 Time 物件的方法
# inside class Time:
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
當你print一個物件時,Python 會呼叫str方法
>>> time = Time(9, 45)
>>> print time
09:45:00
當我編寫新類時,我幾乎總是先編寫 __init__,它使例項化物件更容易,以及 __str__,它對除錯很有用。
為 Point 類編寫一個 str 方法。建立一個 Point 物件並列印它。
透過定義其他特殊方法,你可以指定運算子對使用者定義型別的行為。例如,如果你為時間類定義了一個名為 __add__ 的方法,你就可以使用+運算子對 Time 物件。
定義可能如下所示
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
使用方法如下
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print start + duration
11:20:00
當你將+運算子應用於 Time 物件時,Python 會呼叫 __add__。當你列印結果時,Python 會呼叫 __str__。所以幕後發生了很多事情!
更改運算子的行為使其適用於使用者定義的型別被稱為 運算子過載。對於 Python 中的每個運算子,都存在一個對應的特殊方法,如 __add__。有關更多詳細資訊,請參閱docs.python.org/ref/specialnames.html.
為 Point 類編寫一個 'add' 方法。
在上一節中,我們添加了兩個 Time 物件,但你可能還想將一個整數新增到一個 Time 物件。以下是 __add__ 的一個版本,它檢查其他的型別,並呼叫 add_time 或增加:
# inside class Time:
def __add__(self, other):
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)
def add_time(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
內建函式isinstance接受一個值和一個類物件,如果該值是該類的例項,則返回True。
如果其他是 Time 物件,則 __add__ 會呼叫 add_time。否則,它會假設引數是一個數字,並呼叫增加。此操作稱為 基於型別的分派,因為它根據引數的型別將計算分派到不同的方法。
以下是一些使用+運算子和不同型別的示例
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print start + duration
11:20:00
>>> print start + 1337
10:07:17
不幸的是,這種加法實現不是可交換的。如果整數是第一個運算元,你將得到
>>> print 1337 + start
TypeError: unsupported operand type(s) for +: 'int' and 'instance'
問題是,Python 不是要求 Time 物件新增一個整數,而是要求一個整數新增一個 Time 物件,它不知道該怎麼做。但是,這個問題有一個巧妙的解決方案:特殊方法 __radd__,它代表“右側新增”。當 Time 物件出現在+運算子的右側時,會呼叫此方法。以下是定義
# inside class Time:
def __radd__(self, other):
return self.__add__(other)
使用方法如下
>>> print 1337 + start
10:07:17
為 Point 編寫一個 add 方法,該方法適用於 Point 物件或元組:
- 如果第二個運算元是 Point,則該方法應返回一個新的 Point,其 x 座標為運算元的 x 座標之和, y 座標亦然。
- 如果第二個運算元是元組,則該方法應將元組的第一個元素新增到 x 座標,將第二個元素新增到 y 座標,並返回一個具有結果的新 Point。
基於型別的分派在必要時很有用,但(幸運的是)並不總是必要。通常,你可以透過編寫對具有不同型別引數有效的功能來避免它。
我們為字串編寫的許多函式實際上適用於任何型別的序列。例如,在第 11.1 節中,我們使用了histogram來計算每個字母在一個詞中出現的次數。
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c]+1
return d
此函式也適用於列表、元組,甚至字典,只要s的元素是可雜湊的,以便它們可以用作d.
>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'egg': 1, 'spam': 4}
中的鍵。能夠處理多種型別的函式稱為 多型的。多型性可以促進程式碼重用。例如,內建函式sum
,它將序列的元素相加,只要序列的元素支援加法,它就有效。由於 Time 物件提供了一個add能夠處理多種型別的函式稱為 多型的。多型性可以促進程式碼重用。例如,內建函式:
>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print total
23:01:00
方法,因此它們適用於
一般來說,如果函式內部的所有操作都適用於給定型別,那麼該函式就適用於該型別。
最好的多型性是無意中的那種,即你發現你已經編寫的一個函式可以應用於你從未計劃過的型別。
除錯在程式執行的任何時候,向物件新增屬性都是合法的,但是如果你是一個型別理論的死忠,那麼擁有具有不同屬性集的相同型別的物件是一種可疑的做法。通常,最好在 init 方法中初始化所有物件的屬性。如果你不確定物件是否具有特定屬性,可以使用內建函式hasattr
(參見第 15.7 節)。
>>> p = Point(3, 4)
>>> print p.__dict__
{'y': 4, 'x': 3}
另一種訪問物件屬性的方法是透過特殊屬性 __dict__,它是一個將屬性名稱(作為字串)和值對映起來的字典
def print_attributes(obj):
for attr in obj.__dict__:
print attr, getattr(obj, attr)
出於除錯目的,你可能會發現保留此函式很方便
內建函式print_attributes 遍歷物件字典中的專案,並列印每個屬性名稱及其對應值。getattr 接受一個物件和一個屬性名稱(作為字串),並返回屬性的值。
- 面嚮物件語言
- 一種提供諸如使用者定義類和方法語法之類的功能的語言,這些功能有助於面向物件程式設計。
- 面向物件程式設計
- 一種程式設計風格,其中資料及其操作被組織成類和方法。
- 方法
- 在類定義內部定義並在該類的例項上呼叫的函式。
- 主題
- 方法被呼叫的物件。
- 運算子過載
- 更改運算子的行為,如+,使其適用於使用者定義的型別。
- 基於型別的分派
- 一種程式設計模式,它檢查運算元的型別,併為不同的型別呼叫不同的函式。
- 多型的
- 與能夠處理多種型別的函式有關。
本練習是關於 Python 中最常見且最難發現的錯誤之一的警示故事。
- 為名為Kangaroo的類編寫定義,該類具有以下方法
- 一個
__init__方法,它將名為pouch_contents的屬性初始化為一個空列表。
- 一個名為
put_in_pouch的方法,它接受任何型別的物件並將其新增到pouch_contents中。
- 一個
__str__方法,它返回 Kangaroo 物件及其袋鼠袋內容的字串表示形式。
透過建立兩個Kangaroo物件,並將它們分配給名為kanga和roo的變數,然後新增roo到kanga袋鼠袋內容中,來測試你的程式碼。
- 下載 thinkpython.com/code/BadKangaroo.py。它包含對之前問題的解決方案,但有一個非常嚴重的錯誤。找到並修復該錯誤。
如果你卡住了,可以下載 thinkpython.com/code/GoodKangaroo.py ,它解釋了問題並展示了一個解決方案。
Visual 是一個提供 3-D 圖形的 Python 模組。它並不總是包含在 Python 安裝中,因此你可能需要從你的軟體庫中安裝它,或者如果它不存在,則從 'vpython.org' 安裝。
以下示例建立一個 256 單位寬、長和高的 3-D 空間,並將“中心”設定為點 '(128, 128, 128)'。然後它繪製一個藍色球體。
from visual import *
scene.range = (256, 256, 256)
scene.center = (128, 128, 128)
color = (0.1, 0.1, 0.9) # mostly blue
sphere(pos=scene.center, radius=128, color=color)
color是一個 RGB 元組;也就是說,元素是 0.0 到 1.0 之間的紅-綠-藍級別(參見wikipedia.org/wiki/RGB_color_model).
如果您執行這段程式碼,您應該會看到一個黑色背景和藍色球體的視窗。如果您上下拖動中間按鈕,您可以放大和縮小。您還可以透過拖動右鍵來旋轉場景,但由於世界中只有一個球體,因此很難區分。
以下迴圈建立一個球體立方體
''t = range(0, 256, 51)
for x in t:
for y in t:
for z in t:
pos = x, y, z
sphere(pos=pos, radius=10, color=color)
- 將這段程式碼放在指令碼中並確保它對您有效。
- 修改程式,使立方體中的每個球體都具有與其在 RGB 空間中的位置相對應的顏色。請注意,座標範圍是 0-255,但 RGB 元組範圍是 0.0-1.0。
- 下載thinkpython.com/code/color_list.py並使用 `read_colors` 函式生成系統上可用顏色的列表,包括它們的名稱和 RGB 值。對於每種命名顏色,在與其 RGB 值相對應的位置繪製一個球體。
您可以在以下位置看到我的解決方案:thinkpython.com/code/color_space.py.