跳轉到內容

Think Python/類和方法

來自 Wikibooks,開放世界中的開放書籍

面向物件特性

[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物件,並將它們分配給名為kangaroo的變數,然後新增rookanga袋鼠袋內容中,來測試你的程式碼。

  • 下載 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.

進一步閱讀

[edit | edit source]
華夏公益教科書