Python學習之路8.1-類

VPointer發表於2018-05-29

《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相繫結的變數稱為屬性,比如makemodelyear(不是指那三個形參,而是與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 ~

Python學習之路8.1-類

相關文章