深入瞭解Python的類與模塊化

學習目標

Python 是簡潔、易學、面向對象的編程語言。它不僅擁有強大的原生數據類型,也提供瞭簡單易用的控制語句。本節的主要目標是介紹 Python 中的面向對象編程范式以及模塊化思想,為接下來的學習奠定基礎,本文會完整的介紹學習數據結構和算法所需的 Python 基礎知識及基本思想,並給出相應的實戰示例及解釋。

掌握 Python 面向對象編程的基本概念,並會編寫 Python 自定義類

掌握 Python 模塊化的編程思想

1. 面向對象編程:類

1.1 面向對象編程的基本概念

一個完善的程序是由數據和指令組成的。過程式編程利用“分而治之”的思想,使用函數對數據進行處理,數據與函數之前的關系是松散的,即同樣的數據可以被程序中的所有函數訪問,而一個函數也可以訪問程序中的不同數據。這導致瞭,如果出現異常,需要在整個系統中查找錯誤代碼。

為瞭解決這一問題,面向對象編程 (Object Oriented Programming, OOP) 將系統劃分為不同對象,每個對象包含自身的信息數據以及操作這些數據的方法。例如,每個字符串對象具有字符數據,同時還具有改變大小寫、查找等方法。

面向對象編程使用類描述其所包含的所有對象的共同特性(屬性),即數據屬性(也稱數據成員或成員變量)和功能屬性(也稱成員函數或方法)。

一個類的對象也稱為這個類的一個實例。例如,32 就是一個整數類 int 的對象,可以使用函數 type() 來獲取對象所屬類:

>>> type(32)
<class 'int'>

可以利用內置函數 dir() 查詢類的屬性:

>>> dir(32)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

在繼續講解之前,我們首先來快速介紹下面向對象的三大特性——多態、封裝和繼承。

1.1.1 多態

多態:可對不同類型的對象執行相同的操作。

多態其實很常見,例如列表對象和元組對象都具有 count 方法,使用變量調用 count 方法時,我們無需知道它究竟是列表還是元組,這就是多態的作用。這不僅僅適用於方法,許多內置運算符和函數也使用瞭多態:

>>> [1,3,3,3].count(3)
3
>>> (1,3,3,3).count(3)
3
>>> [1,3]+[1,3]
[1, 3, 1, 3]
>>> 'hello' + ' world!'
'hello world!'

1.1.2 封裝

封裝:對外部隱藏有關對象具體操作的細節。

封裝與多態類似,都屬於抽象原則,都用於處理程序的組成部分而無需關心不必要的細節,但不同的是,多態使我們無需知道對象所屬的類就能調用其方法,而封裝使我們無需知道對象的內部構造就能使用它。例如我們將虛數的實部和虛部作為對象的數據屬性,就是將對象的屬性“封裝”在對象中。

1.1.3 繼承

繼承:用於建立類的層次結構,基於上層的類創建出新類。

如果我們有瞭一些類,再創建新的類時發現與已存在的類十分相似,隻需要添加一些新方法,那麼我們可能不想復制舊類的代碼至新類中,這時我們就要用到繼承瞭。例如,我們有瞭一個 Fruit 類,具有描述外觀的方法 show_shape,如果想要新建一個 Apple 類,除瞭描述外觀外,我們還想知道如何計算總價,那麼我們就可以讓 Apple 類繼承 Fruit 的方法,使得對 Apple 對象調用方法 show_shape 時,將自動調用 Fruit 類的這個方法。

1.2 自定義類

我們已經知道抽象數據類型就是一個由對象以及對象上的操作組成的集合,對象和操作被捆綁為一個整體,不但可以使用對象的數據屬性,還可以使用對象上的操作。操作(在類中稱為方法)定義瞭抽象數據類型和程序其他部分之間的接口。接口定義瞭操作要做什麼,但沒有說明如何做,因此我們可以說抽象的根本目標是隱藏操作的細節。而類就是為瞭實現數據抽象類型。

Python 用關鍵字 class 定義一個類,格式如下,其中方法定義與函數定義語法類似:

class 類名:

方法定義

接下來構建實現抽象數據類型 Imaginary (虛數)的類,用於展示如何實現自定義類。

定義類時首先需要提供構造方法,構造方法定義瞭數據對象的創建方式。要創建一個 Imaginary 對象,需要提供實部和虛部兩部分數據,Python 中,__init__() 作為構造方法名:

class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

形式參數列表的第一項是一個指向對象本身的特殊參數(習慣上通常使用 self ),在調用時不需要提供相應的實際參數,而構造方法中的剩餘參數必須提供相應的實參,使得新創建的對象能夠知道其初始值,與函數定義一樣,可以通過默認值為形參提供默認實參。如在 Imaginary 類中,self.real 定義瞭 Imaginary 對象有一個名為 real 的內部數據對象作為其實部數據屬性,而self.imag 則定義瞭虛部。

創建 Imaginary 類的實例時,會調用類中定義的構造方法,使用類名並且傳入數據屬性的實際值完成調用:

>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a
<__main__.Imaginary object at 0x0000020CF1B80160>

以上代碼創建瞭一個對象,名為 imaginary_a,值為 6+6i,這就是封裝的示例,將數據屬性和操作數據屬性的方法打包在對象中。

除瞭實例化外,類還支持另一操作:屬性引用(包括數據屬性和功能屬性),通過點標記法訪問與類關聯的屬性:

>>> imaginary_a.real
6
>>> imaginary_a.imag
6

除瞭數據屬性外,還需要實現抽象數據類型所需要的方法(功能屬性),需要牢記的是,方法的第一個參數 self 是必不可少的,例如要實現打印實例化的虛數對象,編寫類方法 display()

class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
    
    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))

調用類方法打印實例化的虛數對象:

>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a.display()
6+6i
>>> print(imaginary_a)
<__main__.Imaginary object at 0x0000020CF1B72D90>

可以看到,如果使用 print() 函數隻能打印存儲在變量中的地址,這是由於將對象轉換成字符串的方法 __str__() 的默認實現是返回實例的地址字符串,如果想要使用 print 函數打印對象,需要重寫默認的 __str__() 方法,或者說重載該方法:

>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a.display()
6+6i
>>> print(imaginary_a)
<__main__.Imaginary object at 0x0000020CF1B72D90>

此時如果再次使用 print() 函數,就可以直接打印對象瞭:

>>> imaginary_a = Imaginary(6, 6)
>>> print(imaginary_a)
6+6i

可以重載類中的很多方法,最常見的是重載運算符,這是由於人們習慣使用熟悉的運算符對數據進行運算,這要比使用函數對數據進行運算更加直觀且易於理解,如表達式:8 + 6 / 3,如果用函數則為:add(8, div(6, 3)),顯然前者比後者更加符合習慣。 如果某種類型的對象要使用常見運算符,就必須對這種類型重新定義相應的運算符函數,例如,Python 對於 int 整型、float 浮點型、str 字符串類型等都重新定義瞭乘法運算符函數,對一個類型重新定義運算符函數的也稱“運算符重載”。我們可以編寫 Imaginary 類的 __mul__() 方法重載乘法運算:

class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))
        
    def __str__(self):
        print('{}{:+}i'.format(self.real, self.imag))
    
    def __mul__(self, other):
        new_real = self.real * other.real - self.imag * other.imag
        new_imag = self.real * other.imag + self.imag * other.real
        return Imaginary(new_real, new_imag)

還可以重載其他運算符,如比較運算符 ==,即 __eq__() 方法,重載 Imaginary 類的 __eq__() 方法允許兩個虛數進行比較,查看它們的值是否相等,這也稱為深相等;而根據引用進行判斷的淺相等,隻有兩個變量是同一個對象的引用時才相等:

# shallow_and_deep_equal.py
class ImaginaryFirst:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))

    def __str__(self):
        print('{}{:+}i'.format(self.real, self.imag))
        
class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))

    def __str__(self):
        print('{}{:+}i'.format(self.real, self.imag))
    
    def __eq__(self, other):
        return self.real == other.real and self.imag == self.imag

print('淺相等:隻有兩個變量是同一個對象的引用時才相等。')
imag_1 = imag_2 = ImaginaryFirst(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)
imag_1 = ImaginaryFirst(6, 6)
imag_2 = ImaginaryFirst(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)

print('深相等:兩個變量的值相等即表示對象相等。')
imag_1 = imag_2 = Imaginary(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)
imag_1 = Imaginary(6, 6)
imag_2 = Imaginary(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)

程序運行結果如下所示:

淺相等:隻有兩個變量是同一個對象的引用時才相等。

imag_1 == imag_2   True

imag_1 == imag_2   False

深相等:兩個變量的值相等即表示對象相等。

imag_1 == imag_2   True

imag_1 == imag_2   True

1.3 再談繼承

1.3.1 繼承實例

繼承可以建立一組彼此相關的抽象,能夠建立一個類的層次結構,這樣的關系結構也稱為繼承層次結構,每個類都可以從上層的類繼承屬性。在 Python 中,object 類位於最頂層。

下層的新類會繼承已有類的屬性,同時添加自己特有的一些屬性,這個新的類就稱為“派生類”或“子類”,而原有的類稱為“基類”、“父類”或“超類”。利用父類定義子類,需要在定義的類名後添加圓括號,圓括號內寫入父類名。如果沒有顯式地說明一個類的父類,則默認其父類為 object

例如三角形,包括銳角三角形、直角三角形和鈍角三角形,因此定義類時,除瞭定義一般三角形的類 Triangle 外,還可以定義銳角三角形的類 AcuteTriangle 等,這時我們就可以令 AcuteTriangle 類繼承 Triangle 類:

class Triangle:
    def __init__(self, edge_1, edge_2, edge_3):
        self.edge_1 = edge_1
        self.edge_2 = edge_2
        self.edge_3 = edge_3
    
    def __str__(self):
        return str((self.edge_1, self.edge_2, self.edge_3))
    
    def print_info(self):
        print('The three sides of a triangle are {}, {} and {}'.format(self.edge_1, self.edge_2, self.edge_3))
    
    def perimeter(self):
        return self.edge_1 + self.edge_2 + self.edge_3
        
class AcuteTriangle(Triangle):
    def __init__(self, edge_1, edge_2, edge_3, max_angle):
        # 使用父類構造函數進行初始化
        Triangle.__init__(self, edge_1, edge_2, edge_3)
        self.max_angle = max_angle
    
    def print_info(self):
        Triangle.print_info(self)
        print('The max angle is {}'.format(self.max_angle))
    
    def get_max_angle(self):
        return self.max_angle

可以看到子類除瞭繼承外,還可以:

  • 添加新屬性,例如子類 AcuteTriangle 中新增瞭數據屬性 max_angle 以及方法屬性 get_max_angle
  • 替換(覆蓋)父類中的屬性,例如AcuteTriangle覆蓋瞭父類的__init__()和 print_info() 方法。以 AcuteTriangle.__init__() 方法為例,首先調用 Triangle.__init__() 初始化被繼承的實例變量 self.edge_1,self.edge_2,self.edge_3, 然後初始化 self.max_angle,這個實例變量隻在 AcuteTriangle 實例中才有,而Triangle 實例中沒有。
>>> triangle_a = Triangle(3, 4, 6)
>>> triangle_a.print_info()
The three sides of a triangle are 3, 4 and 6
>>> triangle_b = AcuteTriangle(3, 3, 3, 60)
>>> triangle_b.print_info()
The three sides of a triangle are 3, 3 and 3
The max angle is 60

在子類中可以通過 super() 方法來調用父類的方法,這種方法可以省略父類名:

class AcuteTriangle(Triangle):
    def __init__(self, edge_1, edge_2, edge_3, max_angle):
        # 使用父類構造函數進行初始化
        super().__init__(self, edge_1, edge_2, edge_3)
        self.max_angle = max_angle
    
    def print_info(self):
        super().print_info(self)
        print('The max angle is {}'.format(self.max_angle))
    
    def get_max_angle(self):
        return self.max_angle

使用內置函數 isinstance() 可以檢查一個對象是否是某個類的實例(對象),而要確定一個類是否是另一個類的子類,則可以使用內置方法 issubclass()

>>> triangle_a = Triangle(3, 4, 6)
>>> triangle_b = AcuteTriangle(3, 3, 3, 60)
>>> print(isinstance(triangle_a, Triangle))
True
>>> print(isinstance(triangle_a, AcuteTriangle))
False
>>> print(isinstance(triangle_b, AcuteTriangle))
True
>>> print(isinstance(triangle_b, Triangle))
True
>>> print(issubclass(AcuteTriangle, Triangle))
True
>>> print(issubclass(Triangle, AcuteTriangle))
False

因為類 AcuteTriangle 是從類 Triangle 派生出來的,所以一個類 AcuteTriangle 對象當然也是一個類 Triangle 對象,正如“一個銳角三角形也是一個三角形”。

1.3.2 多繼承

一個類可以繼承多個類的特性,這也稱為多繼承,例如:

class RightTriangle(Triangle):
    def area(self):
        return self.edge_1 * self.edge_2 * 0.5

    def print_name(self):
        print('This is a right triangle!')

class IsoscelesTriangle(Triangle):
    def print_name(self):
        print('This is an isosceles triangle!')

class IsoscelesRightTriangle(RightTriangle, IsoscelesTriangle):
    pass

以上示例中,pass 語句不做任何事,其作用相當於占位符,以等待後續補充代碼;也可以用於語法上需要語句而實際不需要做任何工作的地方。

2. 模塊

我們已經知道,函數和類都是可以重復調用的代碼塊。在程序中使用位於不同文件的代碼塊的方法是:導入 (import) 該對象所在的模塊 (mudule)。

在之前的示例中,我們總是使用 shell,或假設整個程序保存在一個文件中,這在程序比較小時可能沒有什麼問題。但程序變得越來越大時,將程序的不同部分根據不同分類方法保存在不同文件中通常會更加方便。

2.1 導入模塊

Python 模塊允許我們方便地使用多個文件中的代碼來構建程序。模塊就是一個包含 Python 定義和語句的 .py 文件。

例如我們創建一個 hello_world.py 文件,就可以理解為創建瞭一個名為 hello_world 的模塊:

# hello_world.py
def print_hello():
    print('Hello World!')

class Triangle:
    def __init__(self, edge_1, edge_2, edge_3):
        self.edge_1 = edge_1
        self.edge_2 = edge_2
        self.edge_3 = edge_3
    
    def __str__(self):
        return str((self.edge_1, self.edge_2, self.edge_3))
    
    def print_info(self):
        print('The three sides of a triangle are {}, {} and {}'.format(self.edge_1, self.edge_2, self.edge_3))
    
    def perimeter(self):
        return self.edge_1 + self.edge_2 + self.edge_3

可將模塊視為擴展,要導入模塊,需要使用關鍵字 import,導入模塊的一般格式如下:

import module_1[, module_2....]  # 可以同時導入多個模塊

例如在 test.py 文件要導入 hello_world 模塊:

import hello_world

導入的模塊隻要說明模塊名即可,不需要也不能帶有文件擴展名 .py。如果要使用模塊中的對象,如函數、類等,需要用使用句點運算符 (.),即使用“模塊名.對象”進行訪問。例如,使用 hello_worl.Triangle 訪問模塊 hello_world 中的類 Triangle:

# test_1.py
import hello_world
hello_world.print_hello()
tri_a = hello_world.Triangle(3, 4, 5)
print(tri_a)

程序輸出如下所示:

Hello World!

(3, 4, 5)

需要註意的是,導入的模塊要位於相同的目錄層次下,否則需要添加目錄結構,例如,如果 hello_world 位於子目錄 module 下,則需要使用如下方式:

# test_2.py
import module.hello_world
module.test.print_hello()

程序輸出如下所示:

Hello World!

2.2 導入Python標準模塊

Python 提供瞭許多標準模塊,這些模塊文件位於 Python 安裝目錄的 lib 文件夾中。可以像導入自己編寫的模塊一樣導入標準模塊,例如導入 math 模塊,使用其中的對象:

# test_3.py
import math
print('sqrt(4) = ', math.sqrt(4))
print('sin(π/6) = ', math.sin(math.pi /6))

程序輸出如下所示:

sqrt(4) = 2.0

sin(π/6) = 0.49999999999999994

這裡可能大傢會有一個疑問,這裡導入的模塊和當前文件並不在同一目錄下,為什麼不需要使用模塊路徑?這個問題也可以轉換為——當我們使用 import 語句的時候,Python 解釋器是怎樣找到對應的文件的呢?

這就涉及到 Python 的搜索路徑,搜索路徑是由一系列目錄名組成的,Python 解釋器就依次從這些目錄中去尋找所引入的模塊。搜索路徑被存儲在 sys 模塊中的 path 變量中:

>>> import sys
>>> sys.path
['', 'D:\\Program Files\\Python39\\python39.zip', 'D:\\Program Files\\Python39\\DLLs', 'D:\\Program Files\\Python39\\lib', 'D:\\Program Files\\Python39', 'D:\\Program Files\\Python39\\lib\\site-packages'

2.3 單獨導入模塊中所需對象

我們可能不想每次調用模塊中的對象時都指定模塊名,這時,我們可以使用 from module import object,從模塊中單獨導入所需對象,同時使用這個單獨導入的對象時就不需要在前面添加“模塊名.”前綴瞭:

# test_4.py
from math import pi, sin
print('sqrt(4) = ', sqrt(4))
print('sin(π/6) = ', sin(math.pi /6))

2.4 導入模塊中的所有對象

可以通過 from module import *導入模塊中的所有對象,同樣不再需要模塊名前綴:

# test_5.py
from math import *
print('sqrt(4) = ', sqrt(4))
print('sin(π/6) = ', sin(math.pi /6))

不同程序代碼中不可避免地可能會使用瞭同一個名字來命名不同對象,這時就會引起沖突,但如果這些名字屬於不同的模塊,就可以通過模塊名來區分它們,因此為瞭避免名字沖突,應盡量避免使用 from module import object 或 from module import * 導入對象。

2.5 重命名導入模塊或對象

另一種避免名字沖突的方法是重命名導入模塊或對象:

# test_6.py
import math as m
from datetime import date as d
print(d.today())
print('sqrt(4) = ', m.sqrt(4))
print('sin(π/6) = ', m.sin(math.pi /6))

程序輸出如下所示:

datetime.date(2021, 12, 3)

sqrt(4) = 2.0

sin(π/6) = 0.49999999999999994

可以看到附加的好處是可以使用簡寫,減少編碼工作量。

2.6 導入第三方模塊

除瞭標準庫外,Python 也具有規模龐大的第三方庫,覆蓋瞭信息技術幾乎所有領域,這也是 Python 的其中一個巨大優勢。下面以常用可視化庫 matplotlib 為例介紹第三方庫的使用。和標準庫不同,使用第三方庫首先要進行安裝,在 shell 命令中使用 pip 命令可以快速安裝所需庫:

pip install matplotlib

安裝完成後,使用第三方庫就和標準庫沒有任何差別瞭:

# cos_1.py
import math
from matplotlib import pyplot as plt
scale = range(100)
x = [(2 * math.pi * i) / len(scale) for i in scale]
y = [math.cos(i) for i in x]
plt.plot(x, y)
plt.show()

以上就是深入瞭解Python的類與模塊化的詳細內容,更多關於Python 類 模塊化的資料請關註WalkonNet其它相關文章!

推薦閱讀: