Python里的所有數據都是以對象形式存在的,無論是的簡單的數字類型還是復雜的代碼模塊。然而,Python特殊的語法形式巧妙地將實現對象機制的大量細節隱藏起來。輸入 num = 1 就可以創建一個值為 1 的整數對象,並且將這個對象值賦值給變量num。事實上,在Python中,只有當你想要創建屬於自己的對象或者需要修改已有的對象的行為時,才需要關注對象的內部實現細節。
對象既包含數據(變量,更習慣稱之為特性,attribute),也包含代碼(函數,也成為方法)。它是某一類具體事務的特殊實例。例如,整數 7 就是一個包含了加法、乘法之類方法的對象。整數 8 則是另一個對象。這意味着在 Python 里,7和8都屬於一個公共的類,我們稱之為整數類。(字符串、list、dict)
當你想要創建一個別人從來沒有創建過的新對象時,首先必須定義一個類,用以指明該類的對象所包含的內容(特性和方法)。
可以把對象想象成名詞,那么方法就是動詞。對象代表着一個獨立的事物,它的方法則定義了它是如何與其他事物互相作用的。
與模塊不同,你可以同時創建許多同類的對象,他們的特性值可能各不相同。對象就是像是包含了代碼的超級數據結構。
1、術語
property 屬性
attribute 特性
2、使用 class 定義類
想要在Python中創建屬於自己的類使用關鍵字 class 來定義,我們先看個例子。
假設你想要定義一些對象用於記錄聯系人,每個對象對應一個人。首先需要定義 Person 類作為生產對象的模具。在接下來的幾個例子中,我們會不停更新這個類的內容,從最簡單的開始,知道它成為一個可實際使用的類。
首先創建的是最簡單的類,即一個沒有任何內容的空類:
>>> class Person(): ... pass
同函數一樣,用 pass 表示這個類是一個空類。上面這種定義類的方法已經是最簡形式,無法再忽略。你可以通過類名來創建對象,同調用函數一樣:
>>> someone = Person()
在這個例子中,Person()創建了一個Person類的對象,並給它賦值 someone 這個名字。但是,由於我們的Person類是空的,所以由它創建的對象 someone 實際上什么也做不了。實際編程中,你永遠也不會創建這樣一個沒用的類,我在這里只是為了從零開始引出后面每一步的內容。
我們來試着重新定義一下 Person 類。這一次,將 Python 中特殊的對象初始化方法放入其中:
>>> class Person(): ... def __init__(self): ... pass
我承認 init() 和 self 看起來很奇怪,但這就是實際的Python類的定義形式。init() 是 Python 中一個特殊的函數名,用於根據類的定義創建實例對象。self參數指向了這個正在被創建的對象本身。
當你在類生命定義 init() 方法時,第一個參數必須為 self。盡管 self 並不是一個 Python 保留字,但它很常用。
盡管我們添加了初始化方法,但用這個 Person 類創建的對象仍然什么也做不了。接着我們在初始化方法中添加 name 參數:
>>> class Person(): ... def __init__(self, name): ... self.name = name ...
用 Person 類創建一個對象,為 name 特性傳遞一個字符串參數:
>>> hunter = Person("xiao ming")
python執行:
Person.init(huter, “xiao ming”)
self 其實就是代表要實例化的對象,這個例子里是 hunter。
上面這短短的一行代碼實際做了以下工作:
- 查看 Person 類的定義;
- 在內存中實例化(創建)一個新的對象
- 調用對象的 init 方法, 將這個新創建的對象作為 self 傳入,並將另一個參數(’xiao ming’)作為 name 傳入;
- 將 name 的值存入對象;
- 返回這個新的對象;
- 將名字 hunter 與這個對象關聯。
這個新對象與任何其他的python對象一樣。 你可以把它當作列表、元組、字典或集合中的元素,也可以把它當作參數傳遞給函數,或者把它作為函數的返回結果。
我們傳入的 name 參數 作為對象的特性存儲在了對象里。可以直接對它進行讀寫操作:
>>> print('The mighty hunter: ', hunter.name) The mighty hunter: xiao ming
記住,在 Person 類定義的內部, 你可以直接通過 self.name 訪問 name 特性。 而當創建了一個實際的對象后,例如這里的 hunter,需要通過 hunter.name 來訪問他。
在類的定義中, init 並不是必需的。只有當需要區分由該類創建的不同對象時,才需要指定 init 方法。
3、繼承
在你編寫代碼解決實際問題時,經常能找到一些已有的類,它們能夠實現你所需的大部分功能,但不是全部。這時該怎么辦?當然,你可以對這個已有的類進行修改,但這么做很容易讓代碼變得更加復雜,一不留神就可能會破壞原來可以正常工作的功能。
當然,也可以另起爐灶重新編寫一個類:復制粘貼原來的代碼再融入自己的新代碼。但這意味着你需要維護更多的代碼。同時,新類和舊類中實現同樣功能的代碼被分隔在了不同的地方(日后修改時需要改動多處)。
更好的解決方法是利用類的繼承:從已有類中衍生出新的類,添加和修改部分功能。這是代碼復用的一個絕佳的例子。使用繼承得到的新類會自動獲得舊類中的多有方法,而不需要進行任何復制。
你只需要在新類里面定義自己額外需要的方法,或者按照需求對繼承的方法進行修改即可。修改得到的新方法會覆蓋原有的方法。我們習慣將原始的類稱為父類、超類或基類,將新的類稱作孩子類、子類或衍生類。這些術語在面向對象的編程中不加以區分。
現在,我們來試試繼承。首先,定義一個空類 Car。然后,定義一個 Car 的子類 Yugo。定義子類使用的也是 class 關鍵字,不過需要把父類的名字放在子類名字后面的括號里:
>>> class Car(): ... pass ... >>> class Tesla(Car): ... pass ...
接着,為每個類創建一個實力對象:
>>> give_me_a_car = Car()
>>> give_me_a_tesla = Tesla()
子類是父類的一種特殊情況,它屬於父類。在面向對象的術語里,我們經常成 Yugo 是一個 Car。 對象 give_me_a_yugo 是 Yugo 類的一個實例,但它同事集成了 Car 能做到的所有事情。當然,上面的例子中 Car 和 Yugo 就像潛艇上的甲板水手一樣起不到任何實際作用。我們來更新一下類的定義,讓它們發揮點兒作用:
>>> class Car(): ... def exclaim(self): ... print("I'm a Car!") ... >>> class Tesla(Car): ... pass ... >>>
最后,為每一個類各創建一個對象,並調用剛剛聲明的 exclaim 方法:
>>> give_me_a_car = Car() >>> give_me_a_tesla = Tesla() >>> give_me_a_car.exclaim() I'm a Car! >>> give_me_a_tesla.exclaim() I'm a Car!
我們不需要進行任何特殊的操作,Tesla 就自動從 Car 那里繼承了 exclaim() 方法。但事實上,我們並不希望 Tesla 在 exclaim() 方法里面宣稱它是一個 Car,這可能會造成無法區分 Car 和 Tesla。讓我們來看看怎么解決這個問題。
4、覆蓋方法
新創建的子類會自動繼承父類的所有信息。接下來我們來看子類如何替代——覆蓋(override)父類的方法。
>>> class Car(): ... def exclaim(self): ... print("I'm a Car!") ... >>> class Tesla(Car): ... def exclaim(self): ... print("I'm a Tesla!") ...
為每個類創建一個對象:
>>> give_me_a_car = Car()
>>> give_me_a_tesla = Tesla()
執行看下結果:
>>> give_me_a_car.exclaim() I'm a Car! >>> give_me_a_tesla.exclaim() I'm a Tesla!
我們覆蓋了父類的 exclaim() 方法。 在子類中,可以覆蓋任何父類的方法,包括 init()。下面我們使用之前的 Person 類。我們來創建兩個子類,分別代表醫生(MDPerson)和律師(JDPerson)
>>> class Person(): ... def __init__(self, name): ... self.name = name ... >>> class MDPerson(Person): ... def __init__(self, name): ... self.name = "Doctor " + name ... >>> class JDPerson(Person): ... def __init__(self, name): ... self.name = name + ", Esquire" ... >>>
在上面的例子中,子類的初始化方法 init() 接受的參數和父類 Person 一樣,但存儲到對象內部 name 特性的值卻不盡相同:
>>> person = Person('Fudd') >>> doctor = MDPerson('Fudd') >>> lawyer = JDPerson('Fudd') >>> print(person.name) Fudd >>> print(doctor.name) Doctor Fudd >>> print(lawyer.name) Fudd, Esquire >>>
5、添加新方法
子類還可以添加父類中沒有的方法。回到 Car 類 和 Tesla 類,我們給 Tesla 類添加一個新的方法 need_a_push():
>>> class Car(): ... def exclaim(self): ... print("I'm a Car!") ... >>> class Tesla(Car): ... def exclaim(self): ... print("I'm a Tesla!") ... def need_a_push(self): ... print("A little help here?") ...
接着創建一個 Car 和一個 Tesla 對象:
>>> give_me_a_car = Car()
>>> give_me_a_tesla = Tesla()
Tesla 類的對象可以響應 need_a_push()方法:
>>> give_me_a_tesla.need_a_push()
A little help here?
但父類 Car 無法使用該方法:
>>> give_me_a_car.need_a_push() Traceback (most recent call last): File "<ipython-input-98-61aca925ea27>", line 1, in <module> give_me_a_car.need_a_push() AttributeError: 'Car' object has no attribute 'need_a_push'
至此,Yugo終於可以做一些 Car 做不到的事情了。它的與眾不同的特征開始體現了出來。
6、super
我們已經知道如何在子類中覆蓋父類的方法,但如果想要調用父類的方法就要使用 super()。下面的例子將新定義一個新的類 EmailPerson,用於表示有電子郵箱的 Person。首先,來定義熟悉的Person類:
>>> class Person(): ... def __init__(self, name): ... self.name = name ...
下面是子類的定義。注意,子類的初始化方法 init() 中添加了一個額外的 email 參數:
>>> class EmailPerson(Person): ... def __init__(self, name, email): ... super().__init__(name) ... self.email = email ...
在子類中定義 init() 方法時,父類的 init() 方法會被覆蓋。因此在子類中父類的初始化方法並不會被自動調用,我們必須顯式調用它。以上代碼實際上做了這樣幾件事情:
- 通過 super() 方法獲取了父類 Person 的定義。
- 子類的 init() 調用了 Person.init() 方法。它會自動將 self 參數傳遞給父類。因此,你只需傳入其余參數即可。在上面的例子中,Person() 能接受的其余參數指的是 name。
- self.email = email 這行新的代碼才真正起到了將 EmailPerson 與 Person 區分開的作用。
接下來,創建一個 EmailPerson 類的對象:
>>> bob = EmailPerson('Bob Frapples', 'bob@frapples.com')
我們既可以訪問 name 特性,也可訪問 email 特性:
>>> bob.name 'Bob Frapples' >>> bob.email 'bob@frapples.com'
為什么不像下面這樣定義 EmailPerson 類呢?
>>> class EmailPerson(Person): ... def __init__(self, name, email): ... self.name = name ... self.email = email ...
確實可以這么做,但這有悖我么使用繼承的初衷。我們應該使用 super() 來讓 Person 完成他應該做的事情,就像任何一個單純的 Person 對象一樣。除此之外,不這么寫還有另一個好處,如果 Person 類的定義在未來發生改變,使用 super() 可以保證這些改變會自動體現在 EmailPersion類上,而不需要手動修改。
子類可以按照自己的方式處理問題,但如果人需要借助父類的幫助,使用 super() 是最佳的選擇。
7、self
Python 中經常被爭議的一點(除了使用空格外)就是必須把 self 設置為實例方法的第一個參數。Python 使用 self 參數來找到正確的對象所包含的特性和方法。通過下面的例子,我會告訴你調用對象方法背后 Python 實際做的工作。
>>> car = Car() >>> car.exclaim() I'm a Car!
Python 在背后做了一下兩件事:
- 查找 car 對象所屬的類(Car);
- 把 car 對象作為 self 參數傳給 Car 類所包含的 exclaim() 方法。
了解調用機制后,為了好玩,我們甚至可以像下面這樣進行調用,這與普通的調用語法(car.exclaim())效果完全一致:
>>> Car.exclaim(car) I'm a Car!
當然,我們沒有理由使用這種臃腫的語法。
8、使用屬性對特性進行訪問和設置
property 屬性
attribute 特性
有一些面向對象的語言支持私有特性。這些特性無法從對象外部直接訪問,我們需要編寫 getter 和 setter 方法對這些私有特征進行讀寫操作。
Python 不需要 getter 和 setter 方法,因為 Python 里所有特性都是公開的,使用時全憑自覺。如果你不放心直接訪問對象的特性,可以為對象編寫 setter 和 getter 方法。 但更具Python 風格的解決方案是使用屬性(property)。
下面例子中,首先定義一個 Duck 類,他僅包含一個 hidden_name 特性。我們不希望別人能夠直接訪問這個特性,因此需要定義兩個方法:getter 方法(get_name())和 setter方法(set_name())。我們在每個方法中都添加一個 print() 函數,這樣就能方便地知道它們何時被調用。最后,把這些方法設置為 name 屬性:
>>> class Duck(): ... def __init__(self, input_name): ... self.hidden_name = input_name ... def get_name(self): ... print('inside the getter') ... return self.hidden_name ... def set_name(self, input_name): ... print('inside the setter') ... self.hidden_name = input_name ... name = property(get_name, set_name) ...
這兩個新方法在最后一行之前都與普通的 getter 和 setter 方法沒有任何區別,最后一行則把這兩個方法定義為了 name 屬性。 property() 的第一個參數是 getter 方法,第二個參數是 setter 方法。現在,當你嘗試訪問 Duck 類對象的 name 特性時,get_name()會被自動調用:
>>> fowl = Duck('Howard') >>> fowl.name inside the getter 'Howard'
當然,也可以顯式調用 get_name() 方法,它就像普通的 getter 方法一樣:
>>> fowl.get_name() inside the getter 'Howard'
當對 name 特性執行賦值操作時,set_name() 方法會被調用:
>>> fowl.name = 'Daffy' inside the setter >>> fowl.name inside the getter 'Daffy'
也可以顯式調用 set_name() 方法:
>>> fowl.set_name('Daffy') inside the setter >>> fowl.name inside the getter 'Daffy' >>>
另一種定義屬性的方式是使用裝飾器(decorator)。下一個例子會定義兩個不同的方法,它們都叫 name(),但包含不同的裝飾器:
- @property, 用於指示 getter 方法
- @name.setter, 用於指示 setter 方法
-
>>> class Duck(): ... def __init__(self, input_name): ... self.hidden_name = input_name ... @property ... def name(self): ... print('inside the getter') ... return self.hidden_name ... @name.setter ... def name(self, input_name): ... print('inside the setter') ... self.hidden_name = input_name ...
你仍然可以像之前訪問特性一樣訪問 name, 但這里沒有顯式的 get_name() 和 set_name() 方法:
>>> fowl = Duck('Howard') >>> fowl.name inside the getter 'Howard' >>> fowl.name = 'Donald' inside the setter >>> fowl.name inside the getter 'Donald' >>>
實際上,如果有人能猜到我們在類的內部用的特性名是 hidden_name,他仍然可以直接通過 fowl.hidden_name 進行讀寫操作。
在前面幾個例子中,我們都使用 name 屬性指向類中存儲的某一特性(在我們的例子中是 hidden_name)。除此之外,屬性還可以指向一個計算結果值。我們來定義一個 Circle (圓)類,它包含 radius(半徑) 特性以及一個計算屬性 diameter(直徑):
>>> class Circle(): ... def __init__(self, radius): ... self.radius = radius ... @property ... def diameter(self): ... return 2 * self.radius ...
創建一個 Circle 對象,並給 radius 賦予一個初值:
>>> c = Circle(5) >>> c.radius 5
可以像訪問特性(例如 radius)一樣訪問屬性 diameter:
>>> c.diameter 10
真正有趣的還在后面。我們可以隨時改變 radius 特性的值,計算屬性 diameter 會自動根據新的值更新自己:
>>> c.radius = 7 >>> c.diameter 14
如果你沒有指定某一特性的 setter 屬性(@diameter.setter),那么將無法從類的外部對它的值進行設置。這對於那些只讀的特性非常有用:
>>> c.diameter = 20 Traceback (most recent call last): File "<ipython-input-22-dd5da562ba9f>", line 1, in <module> c.diameter = 20 AttributeError: can't set attribute
與直接訪問特性相比,使用 property 還有一個巨大的優勢,如果你改變了某個特性的定義,只需要在類定義里修改相關代碼即可,不需要再每一處調用修改。
9、使用名稱重整保護私有特性
前面的 Duck 列子中, 為了隱藏內部特性,我們曾將其命名為 hiddenname 。 其實,Python 對那些需要刻意隱藏在類內部的特性有自己的命名規范:
由連續的兩個下划線開頭(_)。
我們來把 hidden_name 改名為 __name,如下所示:
>>> class Duck(): ... def __init__(self, input_name): ... self.__name = input_name ... @property ... def name(self): ... print('inside the getter') ... @name.setter ... def name(self, input_name): ... print('inside the setter') ... self.__name = input_name ...
看看代碼是否還能正常工作:
>>> fowl = Duck('Howard') >>> fowl.name inside the getter 'Howard' >>> fowl.name = 'Donald' inside the setter >>> fowl.name inside the getter 'Donald'
看起來沒問題,現在,你無法在外部訪問 __name 特性了:
>>> fowl.__name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Duck' object has no attribute '__name'
這種命名規范本質上並沒有把特性變成私有,但Python確實將它的名字重整了。讓外部的代碼無法使用。如果你是在好奇名稱重整是怎么實現的。我可以偷偷告訴你其中的奧秘:
>>> fowl._Duck__name 'Donald'
發現了么?我們並沒有得到 inside the getter,成功繞過了 getter 方法。盡管如我們所見,這種保護特性的方式並不完美,但它確實能在一定程度上避免我們無意或有意地對特性進行直接訪問。
10、方法的類型
有些數據(特性)和函數(方法)是類本身的一部分,還有一些是由類創建的實例的一部分。
在類的定義中,以 self 作為第一個參數的方法都是實例方法(instance method)。它們在創建自定義類時最常用。實例方法的首個參數是 self,當它被調用時,Python 會把調用該方法的對象作為 self 參數傳入。
與之相對,類方法(class method)會作用於整個類,對類作出的任何改變會對它的所有實例對象產生影響。在類定義內部,用前綴修飾符 @classmethod 指定的方法都是類方法。與實例方法相似,類方法的第一個參數是類本身。在Python中,這個采納數常被寫作 cls,因為全稱 class 是保留字,在這里我們無法使用。下面的例子中,我們為A定義一個類方法來記錄一共有多少個類A的對象被創建:
>>> class A(): ... count = 0 ... def __init__(self): ... A.count += 1 ... def exclaim(self): ... print("I'm an A!") ... @classmethod ... def kids(cls): ... print("A has", cls.count, "little objects.") ... >>> easy_a = A() >>> breezy_a = A() >>> wheezy_a = A() >>> A.kids() A has 3 little objects.
注意,上面的代碼中,我們使用的是 A.count(類特性),而不是 self.count (可能是對象的特性)。在 kids() 方法中,我們使用的是 cls.count,它與 A.count 的作用一樣。
類定義中的方法還存在着第三種類型,它既不會影響類也不會影響類的對象。他們出現在類的定義中僅僅是為了方便,否則他們只能孤零零地出現在代碼的其他地方,這會影響代碼的邏輯性。這種類型的方法被稱作靜態方法(static method),用 @staticmethod 修飾, 它既不需要 self 參數 也不需要 class 參數。 下面列子中的靜態方法是一則 CoyoteWeapon的廣告:
>>> class CoyoteWeapon(): ... @staticmethod ... def commercial(): ... print('This CoyoteWeapon has been brought to you by Acme') ... >>> CoyoteWeapon.commercial() This CoyoteWeapon has been brought to you by Acme >>>
注意,在這個例子中,我們甚至都不用創建任何 CoyoteWeapon 類的對象就可以調用這個方法,語法優雅不失風格!
11、鴨子類型
Python 對實現多態(polymorphism)要求得十分寬松,這意味着我們可以對不同對象調用同名的操作,甚至與用管這些對象的類型是什么。
我們來為上那個 Quote 類設定同樣的初始化方法 init(),然后再添加兩個新函數:
- who() 返回保存的 person 字符串的值
- says() 返回保存的 words 字符串的內容,並添上指定的表點符號。
它們的具體實現如下所示:
>>> class Quote(): ... def __init__(self, person, words): ... self.person = person ... self.words = words ... def who(self): ... return self.person ... def says(self): ... return self.words + '.' ... >>> class QuestionQuote(Quote): ... def says(self): ... return self.words + '?' ... >>> class ExclamationQuote(Quote): ... def says(self): ... return self.words + '!' ... >>>
我們不需要改變 QuestionQuote 或者 ExclamQuote 的初始化方式,因此沒有覆蓋它們的 init()方法。Python 會自動調用父類 Quote 的初始化函數 init() 來存儲實例變量 person 和 words,這就是我們可以在子類 QuestionQuote 和 ExclamationQuote 的對象里訪問 self.words 的原因。
接下來創建一些對象:
>>> hunter = Quote('Elmer Fudd', "I'm hunting rabbits") >>> print(hunter.who(), 'says:', hunter.says()) Elmer Fudd says: I'm hunting wabbits. >>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc") >>> print(hunted1.who(), 'says:', hunted1.says()) Bugs Bunny says: What's up, doc? >>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season") >>> print(hunted2.who(), 'says:', hunted2.says()) Daffy Duck says: It's rabbit season!
三個不同版本的 says() 為上面三種類型提供了不同的相應方式,這是面向對象的語言中多態的傳統形式。Python 在這方面走的更遠一些,無論對象的種類是什么,只要包含 who() 和 says(),你便可以調用它。我們再來定義一個 BabblingBrook 類,他與我們之前的獵人獵物(Quote 類的后代)什么的沒有任何關系:
>>> class BabblingBrook(): ... def who(self): ... return 'Brook' ... def says(self): ... return 'Babble' ... >>> brook = BabblingBrook()
現在,對不同對象執行 who() 和 says() 方法,其中有一個(brook) 與其他類型的對象毫無關聯:
>>> def who_says(obj): ... print(obj.who(), 'says', obj.says()) ... >>> who_says(hunter) Elmer Fudd says I'm hunting wabbits. >>> who_says(hunted1) Bugs Bunny says What's up, doc? >>> who_says(hunted2) Daffy Duck says It's rabbit season! >>> who_says(brook) Brook says Babble
這種方式有事被稱作鴨子類型(duck typing),這個命名源自一句名言:
如果它想鴨子一樣走路,像樣子一樣叫,那么它就是一直鴨子。
———— 以為智者
12、特殊方法
到目前為止,你已經能創建並使用基本對象了。現在再往深鑽研一些。
當我們輸入像 a = 3 + 8 這樣的表達式時,整數 3 和 8 怎么知道如何實現 + 的? 同樣, a 又是怎么知道如何使用 = 來獲取計算結果的? 你可以使用 Python 的特殊方法(special method),有時魔術方法(magic method), 來實現這些操作符的功能。別擔心,它們一點也不復雜。
這些特殊的方法的名稱以雙下划線()開頭和結束。沒錯,你已經見過其中一個:
`init__`,它根據類的定義以及傳入的參數對新創建的對象進行初始化。
假設你有一個簡單的 Word 類,現在想要添加一個 equals() 方法來比較兩個詞是否一致,忽略大小寫。也就是說,一個包含值 ‘ha’ 的 Word 對象與包含 ‘HA’ 的是相同的。
下面的代碼是第一次嘗試,創建一個普通方法 equals()。self.text 是當前 Word 對象所包含的字符串文本,equals() 方法將該字符串與 words (另一個 Word 對象)所包含的字符串做比較:
>>> class Word(): ... def __init__(self, text): ... self.text = text ... ... def equals(self, word2): ... return self.text.lower() == word2.text.lower() ...
接着創建三個包含不通字符串的 Word 對象:
>>> first = Word('ha') >>> second = Word('HA') >>> third = Word('eh')
當字符串 ‘ha’ 和 ‘HA’ 被轉換為小寫形式再進行比較時(我們就是這么做的),他們應該是相等的:
>>> first.equals(second)
True
但字符串 ‘eh’ 無論如何與 ‘ha’也不會相等:
>>> first.equals(third)
False
我們成功定義了 equals() 方法進行小寫轉換並比較。但試想一下,如果能通過 if first == second 進行比較的話豈不更妙?這樣類會更自然,表現得更像一個 Python 內置的類。 好的,我們來試一下,把前面例子中的 equals() 方法的名稱改為 eq() :
>>> class Word(): ... def __init__(self, text): ... self.text = text ... def __eq__(self, word2): ... return self.text.lower() == word2.text.lower() ...
修改就此結束,來看看新的版本能否正常工作:
>>> first = Word('ha') >>> second = Word('HA') >>> third = Word('eh') >>> first == second True >>> first == third False
太神奇了!是不是如同魔術一般?僅需將方法名改為 Python 里進行相等比較的特殊方法名 eq() 即可。下面列出一些常用的魔術方法:
不僅數字類型可以使用像 + (魔術方法 add())和 - (魔術方法 sub())的數學運算符,一些其他的類型也可以使用。例如,Python 的字符類型使用 + 進行拼接,使用 * 進行復制。字符串常見的魔術方法如下:
除了 init() 外,你會發現在編寫類方法時最常用到的是 str(),他用於定義如何打印對象信息。print() 方法,str() 方法以及一些字符串格式化的相關方法都會用到 str()。交互式解釋器則用 repr() 方法輸出變量。如果在你的類既沒有定義 str() 也沒有定義 repr(), Python會輸出類似下面這樣的默認字符串:
>>> first = Word('ha') >>> first <__main__.Word object at 0x10a70b908> >>> print(first) <__main__.Word object at 0x10a70b908>
我們將 str() 和 repr() 方法都添加到 Word 類里,讓輸出的對象信息變得更好看些:
>>> class Word(): ... def __init__(self, text): ... self.text = text ... def __eq__(self, word2): ... return self.text.lower() == word2.text.lower() ... def __str__(self): ... return self.text ... def __repr__(self): ... return 'Word(' + self.text + ')' ... ... >>> first = Word('ha') >>> first Word(ha) >>> print(first) ha >>>
更多關於魔術方法的內容請查看 Python 文檔 https://docs.python.org/3/reference/datamodel.html#special-method-names
13、組合
如果你想要創建的子類在大多數情況下的行為都和父類相似的話,使用繼承是非常不錯的選擇,建立復雜的繼承關系確實很吸引人,但有些時候使用組合(composition)或者聚合(aggregation)更加符合現實的邏輯。一只鴨子是鳥的一種,它有一條尾巴。尾巴並不是鴨子的一種,它是鴨子的組成部分。
>>> class Tail(): ... def __init__(self, length): ... self.length = length >>> class Bill(): ... def __init__(self, description): ... self.description = description >>> class Duck(): ... def __init__(self, bill, tail): ... self.bill = bill ... self.tail = tail ... def about(self): ... print('This duck has a', bill.description, 'bill and a', ... tail.length, 'tail') ... >>> tail = Tail('long') >>> bill = Bill('wide orange') >>> duck = Duck(bill, tail) >>> duck.about() This duck has a wide orange bill and a long tail >>>
14、何時使用類和對象而不是模塊
有一些方法可以幫助你決定是把你的代碼封裝到類里還是模塊里。
- 當你需要許多具有相似行為(方法)但不同狀態(特性)的實例時,使用對象是最好的選擇。
- 類支持繼承,但模塊不支持。
- 如果你想要保證實例的唯一性,使用模塊是最好的選擇。不管模塊在程序中被引用多少次,始終只有一個實例被加載(單例)。
- 如果你有一系列包含多個值的變量,並且它們能作為參數傳入不同的函數,那么最好將它們封裝到類里面。舉個例子,你可能會使用以 size 和 color 為鍵的字典代表一張彩色圖片。你可以在程序中為每張圖片創建不同的字典,並把它們作為參數傳遞給像 scale() 或者 transform() 之類的函數。但這么做的話,一但你想要添加其他的鍵或者函數會變得非常麻煩。為了確保統一性,應該定義一個 Image 類,把 size 和 color 作為特性,把 scale() 和 transform 定義為方法。這么一來,關於一張圖片的所有數據和可執行的操作都存儲在了統一的位置。
- 用最簡單的方式解決問題。使用字典、列表和元組往往要比使用模塊更加簡單、簡潔且快速。而使用類則更為復雜。
創始人 Guido 的建議:
不要過度構建數據結構。盡量使用元組(以及命名元組)而不是對象。盡量使用簡單的屬性域兒不 是 getter/setter 函數…… 內置數據類型是你最好的朋友。盡可能多地使用數字、字符串、元 組、列表、集合以及字典。多看看容器庫提供的類型,尤其是雙端隊列。 —— Guid van Rossum