摘要:被繼承的類稱為父類基類或超類,新的類稱為子類或派生類。但要注意的是,繼承關系應只發生在有較強相互關系的類之間,比如從車類派生出電動車類,沒有從車類派生出哈士奇這種騷操作。
《Python編程:從入門到實踐》筆記。1. 概述
本章主要介紹一種重要的編程思想:面向對象編程,包括了類與對象等概念及操作。
面向對象編程(Object-oriented programming, OOP)是最有效的軟件編寫方法之一。面向對象的思想也是人類自古認識世界的方法,即“分門別類”。而在以往的經驗里,筆者印象最深刻的面向對象思想就是中學生物課本上對自然界的分類:界門綱目科屬種。這里要明白兩個概念:類與對象。類是一個總的抽象概念,是一群相似事物的總括,是一個虛的概念,而這些“事物”便是對象,例如:“狗”這一概念,這就是一個“類”,哪怕是具體到某一個特定的種類,比如哈士奇,這也是個類,只有當真正具體到某一條狗時,比如“你家的哈士奇A”,這才到達了“對象”這一概念,綜上:類是抽象的,對象是實際的。而從類到對象的過程,就叫做類的實例化。
2. 創建和使用類 2.1 創建一個Car類在Python中類名一般采用駝峰命名法,即每個單詞的首字母大寫,而不使用下劃線,實例名和模塊名都采用小寫,用下劃線拼接。并且,不論是在寫函數,類,還是代碼文件,最好都加上一個文檔字符串,比如下面的三引號字符串。
class Car: """一次模擬汽車的簡單嘗試""" def __init__(self, make, model, year): """初始化描述汽車的屬性""" self.make = make self.model = model self.year = year self.odometer_reading = 0 # 里程表 def get_descriptive_name(self): """返回整潔的描述性信息""" long_name = str(self.year) + " " + self.make + " " + self.model return long_name.title() def read_odometer(self): """打印一條指出汽車歷程的消息""" print("This car has " + str(self.odometer_reading) + " miles on it.") def update_odometer(self, mileage): """將里程表讀書設置為指定的值,且禁止讀數回調""" if mileage <= 0: print("Mileage must be bigger than 0!") elif mileage >= self.odometer_reading: self.odometer_reading = mileage else: print("You can"t roll back an odometer!") def increment_odometer(self, miles): """將里程表讀數增加指定的量,且該量必須為正數""" if miles > 0: self.odometer_reading += miles else: print("Mile must be bigger than 0!") def fill_gas_tank(self): """將油箱裝滿""" print("The gas tank has been filled!")
以下有幾點需要注意:
①類中的函數稱為方法,比如上述定義的三個函數;類中與self相綁定的變量稱為屬性,比如make,model,year(不是指那三個形參,而是與self綁定的變量)。
②每一個類必有一個__init()__方法,這個方法被稱為構造方法(在C++中被稱為構造函數,不過不用太糾結到底是“方法”還是“函數”,一個東西放在了不同地方有了不同的名字而已)。當然它也有默認的版本,即只有一個self參數,并且該函數什么也不做,這也表明,你甚至都不用定義這個方法,到時候Python會自動生成并調用默認構造方法,不過“不定義構造方法”這種情況估計也就只有像筆者這樣初學的時候才能遇到 ^_^。
③Python中self參數是類中每個非靜態方法必須要有的形參,且必須放在第一個,它是一個指向實例本身(不是類本身!)的一個引用,讓實例能夠訪問類中的屬性和方法,我們在調用類的方法時不用手動傳入該參數,它會自動被傳入。類中的屬性在類中所有的方法里都能被訪問,這便是通過self參數實現的。如果站在C++的角度理解,self就相當于C++類里的this指針,指向對象自身。
④類中的每個屬性都必須有初始值,哪怕這個值是0,空字符串或者None。比如本例中的四個屬性,前三個屬性的值由用戶傳入,odometer_reading的值被設為了0。
⑤在上述代碼的第一行類名Car后面可帶可不帶小括號,即class Car:這種寫法可行,class Car():這種寫法也可以。
2.2 使用該Car類以下代碼創建了一個Car類的對象,并對該對象進行了簡單的操作。
# 代碼: class Car: -- snip -- # 這不是一個Python語法!這里只是表示省略。 my_new_car = Car("audi", "a4", 2016) print(my_new_car.get_descriptive_name()) my_new_car.read_odometer() # 直接修改屬性 my_new_car.odometer_reading = -100 my_new_car.read_odometer() my_new_car.odometer_reading += -1 my_new_car.read_odometer() # 通過方法修改屬性 my_new_car.update_odometer(-100) my_new_car.read_odometer() my_new_car.increment_odometer(-1) my_new_car.read_odometer() my_new_car.update_odometer(100) my_new_car.read_odometer() my_new_car.increment_odometer(1) my_new_car.read_odometer() # 結果: 2016 Audi A4 This car has 0 miles on it. This car has -100 miles on it. This car has -101 miles on it. Mileage must be bigger than 0! This car has -101 miles on it. Mile must be bigger than 0! This car has -101 miles on it. This car has 100 miles on it. This car has 101 miles on it.
從上述代碼可以看出,Python和C++,Java一樣,也是使用句點表示法來訪問屬性以及調用方法。從上述代碼及結果可以看出,實例的屬性可以直接也可以通過方法進行訪問和修改。
直接訪問對象的屬性可以使操作變得簡單,但這違反了封閉性原則,并且直接修改屬性也不利于規范對屬性的操作。比如代碼中將里程設置為一個負值,且在增加里程時增量也是一個負值,這顯然不符合常理(雖然有時也可以這么做)。而如果將對屬性的操作放入方法中,則可以規范這些操作,如上述的read_odometer(),update_odometer(),increment_odometer()等方法。并且這也是面向對象編程所提倡的做法,盡量不要將屬性直接對外暴露。但可惜的是,Python中任何種類的屬性都能被直接操作。
3. 繼承編寫類時并非總是從零開始,如果要編寫的類是現有類的特殊版本,即有相同或相似的屬性和方法,則可以從現有類繼承(派生)出新的類。被繼承的類稱為“父類”、“基類”或“超類(superclass)”,新的類稱為“子類“或”派生類“。
但要注意的是,繼承關系應只發生在有較強相互關系的類之間,比如從車類派生出電動車類,沒有從車類派生出哈士奇這種騷操作。
以下是從Car類派生出ElectricCar類的代碼:
# 代碼: class Car: -- snip -- class ElectricCar(Car): """電動汽車的獨特之處""" def __init__(self, make, model, year): """初始化父類的屬性,再初始化電動汽車特有的屬性""" super().__init__(make, model, year) self.battery_size = 70 def describe_battery(self): """打印一條描述電池容量的消息""" print("This car has a " + str(self.battery_size) + "-kWh battery.") def fill_gas_tank(self): # 重寫了父類的方法 """電動車沒有油箱""" print("This car doesn"t need a gas tank!") my_audi = Car("audi", "a4", 2018) print(my_audi.get_descriptive_name()) my_audi.fill_gas_tank() print() # 用作空行 my_tesla = ElectricCar("tesla", "model s", 2018) print(my_tesla.get_descriptive_name()) my_tesla.describe_battery() my_tesla.fill_gas_tank() # 結果: 2018 Audi A4 The gas tank has been filled! 2018 Tesla Model S This car has a 70-kWh battery. This car doesn"t need a gas tank!
從以上代碼可以總結出幾點:
①創建子類的實例時,Python首先需要對父類進行初始化操作,通過super()函數返回父類的引用,然后再調用父類的構造方法,即super().__init__(參數列表)。在Python2中,對父類的初始化需要以如下方式初始化父類:
super(ElectricCar, self).__init__(make, model, year)
在Python3中也可以按上述方式來初始化父類,但也可以在單繼承時省略super()函數中的參數。
②子類可以訪問父類的所有屬性,還可以增加新的屬性:my_tesla對象訪問了父類的make, model, year等屬性,并且還增加了battery_size屬性。
③子類可以重寫父類的方法:ElectricCar類重寫了Car類的fill_gas_tank()方法。
這里需要區分兩個概念:重寫(Override)與重載(Overload)
重寫也叫覆蓋,主要是用在繼承上。當繼承關系上的類中有相同的方法,但子類和父類在該方法中的操作不相同時,子類對該方法進行重新編寫,覆蓋掉從父類繼承下來的方法。在調用時,Python會自動判斷該對象是否是派生類來調用該方法相應的實現。正是有了重寫,面向對象中多態(Polymorphism)這一特性才得以實現。
重載主要用于函數(方法)。在像C/C++,Java這樣的語言中,可以有多個同名的函數,但參數列表必須不相同,比如參數個數,參數類型不相同。這些語言則根據參數列表來區分到底調用的是同名函數中的哪一個函數。但重載并不屬于多態性!這些語言在編譯源文件的時候,會根據參數列表來對同名函數生成不同的函數名(具體方法就是添加前綴或后綴),然后將源代碼中的這些同名函數都替換成新函數名,所以重載并不屬于多態。但是Python中并沒有函數重載這種說法!因為Python有關鍵字參數和可變參數這種神器(當然C++也有變長參數,它用三個點表示,不知道Python可變參數的底層實現是不是就和C++的變長參數有關)。
然而這都不重要!明白重寫和重載的概念,會用就行了,至于這倆和多態究竟有沒有關系并不重要,至今網上對這倆與多態的關系都沒有一個準確的說法。筆者以前看C++的書的時候記得專門把重載的底層實現給提了出來(哪本書忘了),但筆者才疏學淺,暫不清楚重寫在編譯時是個什么情況,說不定也是靠生成新函數名并替換,如果這樣的話,那重載也可以算多態了,不過這只是筆者的猜測!感興趣的小伙伴可自行研究這倆在編譯時的情況。
之所以把這倆多帶帶提出來,主要是好多人在考研復試或者找工作面試的時候載到了這個概念上。尤其是考研,考研復試似乎更傾向于重寫屬于多態,重載不屬于多態。
3.1 將實例用作屬性使用代碼模擬實物時,隨著開發的進展,勢必一個類的屬性和方法將會越來越多,單單一個類的代碼就會越來越長。這時可以考慮是否能將其中一部分代碼多帶帶提取出來作為一個新的類。比如前面的ElectricCar類里的電池就可以多帶帶提出來作為一個類。
# 代碼: class Car: -- snip -- class Battery: """一次模擬電動汽車電池的簡單嘗試""" def __init__(self, battery_size=70): """初始化電池的屬性""" self.battery_size = battery_size def describe_battery(self): """打印一條描述電池容量的信息""" print("This car has a " + str(self.battery_size) + "-kWh battery.") def get_range(self): """輸出電池的續航里程""" if self.battery_size == 70: miles = 240 elif self.battery_size == 85: miles = 270 message = "This car can go approximately " + str(miles) + " miles on a full charge." print(message) class ElectricCar(Car): def __init__(self, make, model, year): super().__init__(make, model, year) self.battery = Battery() my_tesla = ElectricCar("tesla", "model s", 2018) print(my_tesla.get_descriptive_name()) my_tesla.battery.describe_battery() my_tesla.battery.get_range() # 結果: 2018 Tesla Model S This car has a 70-kWh battery. This car can go approximately 240 miles on a full charge.
模擬復雜的實物時,需要解決一些有趣的問題,比如續航里程是電池的屬性還是汽車的屬性呢?如果只描述一輛車,那將get_range()方法放入Battery()中并無不妥,但如果要描述整個汽車產品線呢?比如這一款車型能跑多遠,那也許將該方法放入ElectricCar類則比較合適。但不管怎樣,這里強調的是應該站在一個更高的邏輯層面考慮問題。
4. 從模塊導入類與上一篇寫關于函數的文章相似,類也可以多帶帶形成模塊。可以一個類就是一個模塊,也可以多個類(一般是相關聯的類)放入一個模塊。比如將上述的Car類多帶帶放在一個文件中,除去此類的代碼,其他代碼均刪除,最后將該文件命名為car.py(注意這里的文件名是小寫的)。然后再在程序中帶入該類:
from car import Car # 如果命名有沖突,也可以給Car類起個別名 # from car import Car as C my_new_car = Car("audi", "a4", 2018) print(my_new_car.get_descriptive_name()) my_new_car.odometer_reading = 23 my_new_car.read_odometer()
也可以將多個相關聯的類放入同一個文件中,形成一個模塊,比如上面的Car類,ElectricCar類和Battery類,將該文件命名為cars.py,最后導入該文件:
from cars import Car, ElectricCar my_beetle = Car("volkswagen", "beetle", 2018) my_tesla = ElectricCar("tesla", "model s", 2018) -- snip -- # 后面的代碼和之前的類似,不在贅述
也可以將整個模塊導入,并使用句點表示法使用模塊中的類:
import cars my_car = car.Car("volkswagen", "beetle", 2018) my_tesla = car.ElectricCar("tesla", "model s", 2018)
還可以導入模塊中的所有類(不推薦此法,容易產生命名沖突!),此時便不需要使用句點表示法。
from cars import * my_beetle = Car("volkswagen", "beetle", 2018)
還可以在模塊中導入另一個模塊,比如,將Car類多帶帶放在一個文件中形參一個模塊,命名為car.py,再新建一個模塊electric_car.py用于存放Battery類和ElectricCar類,并在該模塊中帶入Car類:
from car import Car class Battery: -- snip -- class ElectricCar(Car): -- snip --
最后在執行文件的源代碼中根據需要導入類:
# 這是書中導入兩個類的代碼 from car import Car from electric_car import ElectricCar my_car = Car("audi", "a4", 2018) my_tesla = ElectricCar("tesla", "model s", 2018)
之前讀到這的時候覺得能不能像以下這樣的方式導入Car類:
from electric_car import Car, ElectricCar my_car = Car("audi", "a4", 2018) my_tesla = ElectricCar("tesla", "model s", 2018)
后來親測,這樣做也是可以的。那問題就來了,像書中那樣的導入方式是不是發生了代碼的覆蓋呢?哪種導入的效率更高呢?筆者在這里還有點懵,后續再更新吧。
模塊導入的方法還有很多,甚至能直接從GitHub導入模塊,上述的導入方式只是皮毛。最后用一個從標準庫導入OrderedDict類的示例結束本文。之前版本的Python中普通字典類是不確保鍵值對之前的順序的,想要確保順序就得使用OrderedDict類。但現在從3.6版本起,Python也確保了普通字典里鍵值對也是有序的了,但是為了兼容性考慮(有可能你的代碼還要運行在3.6之前的版本),目前還是建議使用OrderedDict類。
# 代碼: from collections import OrderedDict favorite_languages = OrderedDict() favorite_languages["jen"] = "python" favorite_languages["sarah"] = "c" favorite_languages["edward"] = "ruby" favorite_languages["phil"] = "python" for name, language in favorite_languages.items(): print(name.title() + ""s favorite_language is " + language.title()) # 結果: Jen"s favorite_language is Python Sarah"s favorite_language is C Edward"s favorite_language is Ruby Phil"s favorite_language is Python
迎大家關注我的微信公眾號"代碼港" & 個人網站 www.vpointer.net ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/41794.html
摘要:第行把具名元組以的形式返回。對序列使用和通常號兩側的序列由相同類型的數據所構成當然不同類型的也可以相加,返回一個新序列。從上面的結果可以看出,它雖拋出了異常,但仍完成了操作查看字節碼并不難,而且它對我們了解代碼背后的運行機制很有幫助。 《流暢的Python》筆記。接下來的三篇都是關于Python的數據結構,本篇主要是Python中的各序列類型 1. 內置序列類型概覽 Python標準庫...
摘要:在類的成員函數中訪問實例屬性需要以為前綴。但提供一種對私有成員的訪問方式對象名類名私有成員類中保護對象類中系統定義的特殊成員類中私有成員多態列表項目 Python學習第一天 類與對象 python的成員函數在,默認有一個self參數,這是類的成員函數與普通函數的主要區別,self,位于參數列表的開頭,self也代表類的實例(對象)自身,可以使用self引用類中的屬性和成員函數。在...
摘要:本章主要是對上一章類的補充。對于多態的補充子類可以被看成是父類的類型,但父類不能被看成是子類的類型。仍然以類為例,動物里有哺乳動物,卵生動物,有能飛的動物和不能飛的動物,這是兩種大的分類方式。一般在中,以為結尾類的都作為接口。 《Python編程:從入門到實踐》筆記。本章主要是對上一章Python類的補充。 1. 從一個類派生出所有類 上一篇文章說道Python類的定義與繼承一般是如下...
摘要:本篇繼續學習之路,實現更多的特殊方法以讓自定義類的行為跟真正的對象一樣。之所以要讓向量不可變,是因為我們在計算向量的哈希值時需要用到和的哈希值,如果這兩個值可變,那向量的哈希值就能隨時變化,這將不是一個可散列的對象。 《流暢的Python》筆記。本篇是面向對象慣用方法的第二篇。前一篇講的是內置對象的結構和行為,本篇則是自定義對象。本篇繼續Python學習之路20,實現更多的特殊方法以讓...
摘要:使用抽象基類顯示表示接口如果類的作用是定義接口,應該將其明確定義為抽象基類。此外,抽象基類可以作為其他類的唯一基類,混入類則決不能作為唯一的基類,除非這個混入類繼承了另一個更具體的混入這種做法非常少見。 《流暢的Python》筆記本篇是面向對象慣用方法的第五篇,我們將繼續討論繼承,重點說明兩個方面:繼承內置類型時的問題以及多重繼承。概念比較多,較為枯燥。 1. 繼承內置類型 內置類型...
閱讀 1938·2021-11-24 09:39
閱讀 3277·2021-09-22 14:58
閱讀 1162·2019-08-30 15:54
閱讀 3315·2019-08-29 11:33
閱讀 1788·2019-08-26 13:54
閱讀 1598·2019-08-26 13:35
閱讀 2468·2019-08-23 18:14
閱讀 762·2019-08-23 17:04