Python學習之路8.1-類

《Python編程:從入門到實踐》筆記。
本章主要介紹一種重要的編程思想:面向對象編程,包括了類與對象等概念及操做。

1. 概述

面向對象編程(Object-oriented programming, OOP)是最有效的軟件編寫方法之一。面向對象的思想也是人類自古認識世界的方法,即「分門別類」。而在以往的經驗裏,筆者印象最深入的面向對象思想就是中學生物課本上對天然界的分類:界門綱目科屬種。這裏要明白兩個概念:類與對象。類是一個總的抽象概念,是一羣類似事物的總括,是一個虛的概念,而這些「事物」即是對象,例如:「狗」這一律念,這就是一個「類」,哪怕是具體到某一個特定的種類,好比哈士奇,這也是個類,只有當真正具體到某一條狗時,好比「你家的哈士奇A」,這纔到達了「對象」這一律念,綜上:類是抽象的,對象是實際的。而從類到對象的過程,就叫作類的實例化python

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綁定的變量)。ruby

②每個類必有一個__init()__方法,這個方法被稱爲構造方法(在C++中被稱爲構造函數,不過不用太糾結究竟是「方法」仍是「函數」,一個東西放在了不一樣地方有了不一樣的名字而已)。固然它也有默認的版本,即只有一個self參數,而且該函數什麼也不作,這也代表,你甚至都不用定義這個方法,到時候Python會自動生成並調用默認構造方法,不過「不定義構造方法」這種狀況估計也就只有像筆者這樣初學的時候才能遇到 ^_^。微信

③Python中self參數是類中每一個非靜態方法必需要有的形參,且必須放在第一個,它是一個指向實例自己(不是類自己!)的一個引用,讓實例可以訪問類中的屬性和方法,咱們在調用類的方法時不用手動傳入該參數,它會自動被傳入。類中的屬性在類中全部的方法裏都能被訪問,這即是經過self參數實現的。若是站在C++的角度理解,self就至關於C++類裏的this指針,指向對象自身。app

④類中的每一個屬性都必須有初始值,哪怕這個值是0,空字符串或者None。好比本例中的四個屬性,前三個屬性的值由用戶傳入,odometer_reading的值被設爲了0。dom

⑤在上述代碼的第一行類名Car後面可帶可不帶小括號,即class Car:這種寫法可行,class Car():這種寫法也能夠。ide

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 ~

相關文章
相關標籤/搜索