Python 3 快速入門 3 —— 模組與類

WINLSR 發表於 2021-12-02
Python

本文假設你已經有一門物件導向程式語言基礎,如Java等,且希望快速瞭解並使用Python語言。本文對重點語法和資料結構以及用法進行詳細說明,同時對一些難以理解的點進行了圖解,以便大家快速入門。一些較偏的知識點在大家入門以後根據實際需要再查詢官方文件即可,學習時切忌鬍子眉毛一把抓。同時,一定要跟著示例多動手寫程式碼。學習一門新語言時推薦大家同時去刷leetcode,一來可以快速熟悉新語言的使用,二來也為今後找工作奠定基礎。推薦直接在網頁上刷leetcode,因為面試的時候一般會讓你直接在網頁編寫程式碼。leetcode刷題路徑可以按我推薦的方式去刷。以下程式碼中,以 >>>... 開頭的行是互動模式下的程式碼部分,>?開頭的行是互動模式下的輸入,其他行是輸出。python程式碼中使用 #開啟行註釋。

模組

模組是包含 Python 定義和語句的檔案,檔案字尾名為 .py,檔名即是模組名。在pycharm中建立名為python-learn的專案,然後建立fib.py檔案,並輸入以下程式碼後儲存:

# 斐波拉契數列
def print_fib(n: int) -> None:
    """列印斐波拉契數列

    :param n: 數列截至範圍
    :return: None
    """
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

def get_fib(n: int) -> list:
    """獲取斐波拉契數列

    :param n: 數列截至範圍
    :return: 存有數列的list
    """
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

接著,開啟pycharm中的Python Console,使用import語句匯入該模組:

# 匯入 fib 模組
>>> import fib
# 使用 fib 模組中定義的函式
>>> fib.print_fib(10)
0 1 1 2 3 5 8 
>>> fib.get_fib(10)
[0, 1, 1, 2, 3, 5, 8]

# 如果經常使用某個函式,可以把它賦值給區域性變數
>>> print_fib = fib.print_fib
>>> print_fib(10)
0 1 1 2 3 5 8 

# 每個模組中都有一個特殊變數 __name__ 記錄著模組的名字
>>> print(fib.__name__)
fib

使用as關鍵字為匯入的模組指定別名:

# 匯入 fib 並指定別名為 fibonacci
>>> import fib as fibonacci
>>> fibonacci.get_fib(10)
[0, 1, 1, 2, 3, 5, 8]

# 模組名依然為 fib
>>> print(fibonacci.__name__)
fib

當我們通過import語句匯入模組時:

  • 首先查詢要匯入的模組是否為內建模組;
  • 沒找到就會根據sys.path(list)中的路徑繼續查詢。(sys.path預設值包含:當前路徑、環境變數PYTHONPATH中的路徑等)

檢視sys.path的值:

>>> print("======= start =======")
... for path in sys.path:
...     print(path)
... print("=======  end  =======")
======= start =======
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pydev
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pycharm_display
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\third_party\thriftpy
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pydev
C:\software\anaconda3\envs\python-learn\python310.zip
C:\software\anaconda3\envs\python-learn\DLLs
C:\software\anaconda3\envs\python-learn\lib
C:\software\anaconda3\envs\python-learn
C:\software\anaconda3\envs\python-learn\lib\site-packages
C:\software\jetbrains\PyCharm 2021.2.3\plugins\python\helpers\pycharm_matplotlib_backend
D:\code\python\python-learn  # fib 模組所在目錄
D:/code/python/python-learn  # 對應linux路徑形式
=======  end  =======

當我們要匯入的模組路徑不在sys.path中時,通過appendextend函式可以將目標路徑手動加入sys.path中。前面的例子裡,為什麼我們沒有手動將專案路徑加入sys.path中就可以匯入fib模組呢?答案是pycharm幫我們做了。在專案中,當我們開啟Python Console時,pycharm執行了以下指令碼:

sys.path.extend(['D:\\code\\python\\python-learn', 'D:/code/python/python-learn'])

注意:為了保證執行效率,每次直譯器會話只匯入一次模組。如果更改了模組內容,必須重啟直譯器;僅互動測試一個模組時,也可以使用 importlib.reload(),例如 import importlib; importlib.reload(modulename)

以指令碼方式執行模組

.py檔案(模組)還可以通過python直譯器以指令碼的方式執行。在pycharm專案中開啟Terminal並執行以下命令可以解釋執行fib模組(也可點選圖形介面的執行按鈕):

python fib.py

執行fib.py時,直譯器從上到下依次解釋執行,由於程式碼中沒有任何輸出動作所以終端沒有任何輸出。

當一個.py檔案(模組)被當作指令碼執行時,會被認為是程式的入口,類似於其他語言中的main函式,於是python直譯器會將該模組的特殊變數__name__置為__main__

現在,我們給fib.py檔案新增一些內容:

# 斐波拉契數列
def print_fib(n: int) -> None:
    """列印斐波拉契數列

    :param n: 數列截至範圍
    :return: None
    """
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

def get_fib(n: int) -> list:
    """獲取斐波拉契數列

    :param n: 數列截至範圍
    :return: 存有數列的list
    """
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

# 只有當前檔案(模組)被當作指令碼執行時 if 語句才為真
if __name__ == "__main__":
    import sys
    # argv[0]始終為檔名
    print(sys.argv[0], end=" ")
    # 傳入的第一個引數
    print(sys.argv[1], end=" ")
    # 傳入的第二個引數
    print(sys.argv[2])
    # 測試 print_fib 函式
    print_fib(int(sys.argv[1]))
    # 測試 get_fib 函式
    fibSeries = get_fib(int(sys.argv[2]))
    print(fibSeries)

輸入以下命令並執行:

PS D:\code\python\python-learn> python fib.py 10 20
fib.py 10 20
0 1 1 2 3 5 8
[0, 1, 1, 2, 3, 5, 8, 13]

如果你瞭解Java,那麼Python中的包你就不會感到陌生。包是名稱空間的一種實現方式,不同包中的同名模組互不影響。包的寫法類似於A.B.CA是包,BA的子包,C可以是B的子包也可以是模組。包在磁碟上的表現就是目錄或者說是路徑,以包結構A.B.C為例,若C為模組,那麼對應的路徑為專案路徑/A/B/C.py。同時Python只把含 __init__.py 檔案的目錄當成包。(後面解釋這個檔案的用處)

以之前建立的python-learn專案為例,在根目錄下建立包com.winlsr,然後將fib.py移動到com.winlsr下,目錄結構如下:

image-20211129185325055

從包中匯入fib模組:

# 匯入 fib 模組
>>> import com.winlsr.fib
# 使用時必須引用模組的全名
>>> com.winlsr.fib.print_fib(10)
0 1 1 2 3 5 8 

使用from package import ...匯入模組、函式(建議重啟一下直譯器):

# 匯入 fib 模組
>>> from com.winlsr import fib
# 使用時直接輸入模組名
>>> fib.print_fib(10)
0 1 1 2 3 5 8 

# 直接匯入 fib 模組中的 print_fib 函式
>>> from com.winlsr.fib import print_fib
# 直接使用方法即可
>>> print_fib(10)
0 1 1 2 3 5 8 

使用 from package import item 時,item 可以是包的子模組(或子包),也可以是包中定義的函式、類或變數等其他名稱。使用 import item 時,item 只可以是模組或包。有的同學會疑問,匯入包有什麼用?以當前專案為例,我們匯入com包(建議重啟一下直譯器):

# 匯入 com 包
>>> import com
>>> com
# 包其實也是模組,對應的檔案是包下的 __init__.py 檔案?
<module 'com'="" from="" 'd:\\code\\python\\python-learn\\com\\__init__.py'="">

# 驗證猜想:
# 將該 __init__.py 檔案中新增如下內容
def print_info():
    print("name :", __name__)

# 重啟直譯器後再次匯入 com 包
>>> import com
# 成功呼叫 __init__.py 檔案中定義的函式,猜想正確
>>> com.print_info()
name : com

從包中匯入 *

from package import *不會匯入package中的任何模組或子包,除非你在該package下的__init__.py檔案中新增了如下顯示說明:

__all__ = ["子模組名1", "子包名1"]

新增該說明後,執行from package import *語句會匯入指定的子模組、子包。

同樣以之前建立的python-learn專案為例,我們執行如語句(建議重啟一下直譯器):

# 希望匯入 com.winlsr 包下的 fib 模組
>>> from com.winlsr import *
# 發現並沒有匯入
>>> fib
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'fib' is not defined

com.winlsr下的__init__.py檔案中新增如下內容:

__all__ = ["fib"]

執行同樣的語句(建議重啟一下直譯器):

>>> from com.winlsr import *
# 匯入成功
>>> fib
<module 'com.winlsr.fib'="" from="" 'd:\\code\\python\\python-learn\\com\\winlsr\\fib.py'="">

注意:通常不建議採用該小結講解的方法匯入模組或子包,而是採用from package import specific_submodule

dir()函式

內建函式 dir()用於查詢模組或子包中定義的名稱(變數、模組(子包)、函式等),返回結果是經過排序的字串列表。沒有引數時,dir()列出當前定義的名稱。

>>> from com.winlsr import fib
>>> dir(fib)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'get_fib', 'print_fib']

>>> dir()
['__builtins__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'fib', 'sys']

封裝

Python中定義類使用class關鍵字,類中可以定義變數和函式,起到封裝的作用。沿用之前建立的專案,在com.winlsr包中建立rectangle模組並定義矩形類:

class Rectangle:
    # 一個類只能有一個建構函式
    def __init__(self):    # 無參構造,self 類似於 this,指向例項物件本身
        self.length = 0.0  # 建構函式中對例項物件的成員變數width和length進行初始化
        self.width = 0.0   # 只有建立的例項才有這兩個變數

    # 計算面積
    def area(self):
        return self.length * self.width
    # 計算周長
    def perimeter(self):
        return (self.length + self.width) * 2

開啟Python Console,輸入以下語句來使用Rectangle類:

>>> from com.winlsr.rectangle import Rectangle
# 使用無參構造建立類的例項物件
>>> rec = Rectangle()
# 呼叫rec例項的方法
>>> rec.perimeter()
0.0
>>> rec.area()
0.0
>>> rec.length
0.0
# Rectangle 類中沒有length這個成員變數,只有例項物件中才有
>>> Rectangle.length
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: type object 'Rectangle' has no attribute 'length'

Python中例項物件的成員變數也可以不在__init__()中定義和初始化,而是直接在類中定義並初始化。原來的Rectangle類可以改寫為:

class Rectangle:
    # 成員變數直接定義在類中並初始化,類和例項都有兩個變數
    length = 0.0
    width = 0.0

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return (self.length + self.width) * 2

注意,如果變數直接定義在類中,那麼建立例項時,例項中的變數是中變數的淺拷貝。重啟直譯器執行以下語句:

>>> from com.winlsr.rectangle import Rectangle
>>> rec0 = Rectangle()
>>> rec0.width is Rectangle.width
True
>>> rec1 = Rectangle()
>>> rec1.length is Rectangle.length
True

例項rec0rec1建立後的記憶體分佈如下:

image-20211201101121363

對例項rec0rec1中的成員變數進行修改:

# 修改例項的成員變數
>>> rec0.width = 1.0
... rec0.length = 2.0
... rec1.width = 3.0
... rec1.length = 4.0
# 列印
>>> print(Rectangle.width)
... print(Rectangle.length)
... print(rec0.width)
... print(rec0.length)
... print(rec1.width)
... print(rec1.length)
0.0
0.0
1.0
2.0
3.0
4.0

根據結果可以發現rec0rec1例項之間的成員變數(不可變 immutable 型別)相互之間不影響,修改後它們的記憶體分佈如下:

image-20211201110133650

注意,如果直接定義在類中的變數是可變(mutable)型別,使用時就應該謹慎。如下,依然在com.winlsr包中建立actor模組並定義演員類:

class Actor:
    # 參演的電影
    movies = []

    def get_movies(self):
        return self.movies

    def add_movie(self, movie):
        self.movies.append(movie)

使用Actor類:

>>> from com.winlsr.actor import Actor
... lixiaolong = Actor()
... xuzheng = Actor()
... lixiaolong.add_movie("猛龍過江")
... xuzheng.add_movie("我不是藥神")
... print(lixiaolong.get_movies())  
... print(xuzheng.get_movies())
# 發現兩個物件的 movies list 是共享的
['猛龍過江', '我不是藥神']
print(xuzheng.get_movies())
['猛龍過江', '我不是藥神']

以上情況是因為多個Actor類的例項中的movies變數(引用)指向同一個list物件,例項lixiaolongxuzheng記憶體分佈如下:

建立後例項後,呼叫add_movie()之前的記憶體分佈:

image-20211130222432854

呼叫add_movie()之後的記憶體分佈:

image-20211130223038163

為了使每個演員的參演電影列表相互獨立,在建立Actor類的例項時應該為每個例項建立一個新的list

class Actor:
    # 參演的電影
    # 該語句沒有用,可以刪掉。例項在建立時會通過建構函式改變指向movies的指向
    movies = []

    def __init__(self):
        self.movies = []

    def get_movies(self):
        return self.movies

    def add_movie(self, movie):
        self.movies.append(movie)

重啟直譯器,再次執行以下語句:

>>> from com.winlsr.actor import Actor
... lixiaolong = Actor()
... xuzheng = Actor()
... lixiaolong.add_movie("猛龍過江")
... xuzheng.add_movie("我不是藥神")
... print(lixiaolong.get_movies())  
... print(xuzheng.get_movies())
['猛龍過江']
['我不是藥神']

綜上,根據前面的示例,如果你不希望多個例項之間共享變數,建議直接將變數定義在__init__函式中。最後,Python支援靜態語言不支援的例項屬性動態繫結特性:

# 給 lixiaolong 這個例項動態新增 age 屬性
>>> lixiaolong.age = 41
>>> lixiaolong.age
41
# 不影響其他例項
>>> xuzheng.age
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'Actor' object has no attribute 'age'

訪問控制

前面的例項中,我們可以通過instance.var的形式來直接訪問例項的成員變數。但通常我們不希望例項中的成員變數被直接訪問,而是通過gettersetter來訪問,這需要我們將成員變數設定為private

Python中:

  • 帶有一個下劃線的變數,形如_var應該被當作是 API (常用於模組中)的非公有部分 (函式或是資料成員)。雖然可以正常訪問,但我們應遵循這樣一個約定;
  • 類中私有成員變數應當用兩個字首下劃線,至多一個字尾下劃線標識,形如:__var。但該變數並不是真正的不能訪問,這是因為Python實現的機制是”名稱改寫“。這種機制在執行時會將__var改為_classname__var,但你仍然可以通過_classname__var來訪問。
  • 形如__var__的變數是特殊變數,可以訪問,但通常我們不需要定義此類變數。

實驗驗證,將Rectangle類改為如下程式碼:

class Rectangle:
    def __init__(self, length, width):
        self.__length = length
        self.__width = width

    def area(self):
        return self.__length * self.__width

    def perimeter(self):
        return (self.__length + self.__width) * 2

重啟直譯器,執行如下語句:

>>> from com.winlsr.rectangle import Rectangle
>>> rec = Rectangle(12.0, 24.0)
# 直接訪問私有變數 __width,失敗
>>> print(rec.__width)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'Rectangle' object has no attribute '__width'
# 訪問私有變數改寫後的名稱 _Rectangle__width,成功
>>> print(rec._Rectangle__width)
24.0

對應Python Console如下:

image-20211201142925442

繼承

Python繼承語法如下,所有類都預設繼承:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-n>

下面我們在com.winlsr包下建立person模組並定義Person類,作為ActorTeacher類的基類:

class Person:
    def __init__(self, name, id_number):
        self.__name = name
        self.__id_number = id_number

    def set_name(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

    def set_id_number(self, id_number):
        self.__id_number = id_number

    def get_id_number(self):
        return self.__id_number

com.winlsr包下建立actor模組並定義Actor類,它是Person類的派生類:

from .person import Person

class Actor(Person):

    def __init__(self, name, id_number):
        self.__movies = []
        # 三種呼叫父類建構函式的方式
        super().__init__(name, id_number)
        # super(Actor, self).__init__(name, id_number)
        # Person.__init__(self, name, id_number)

    def add_movie(self, movie):
        self.__movies.append(movie)

    def print_info(self):
        print(self.get_name(), self.get_id_number(), self.__movies, sep=" : ")

com.winlsr包下建立teacher模組並定義Teacher類,它也是Person類的派生類:

from .person import Person

class Teacher(Person):
    
    # 無建構函式,建立物件時會呼叫父類建構函式

    def print_info(self):
        print(self.get_name(), self.get_id_number(), sep=" : ")

重啟直譯器,執行如下語句:

>>> from com.winlsr.actor import Actor
>>> actor = Actor("xuzheng", 123456789)
>>> actor.add_movie("我不是藥神")
>>> actor.print_info()
xuzheng : 123456789 : ['我不是藥神']

>>> from com.winlsr.teacher import Teacher
>>> teacher = Teacher()
# Teacher 類中沒有定義 __init__(),這裡會呼叫父類 Person 的構造並傳入引數
>>> teacher = Teacher("lsl", 123459876)
>>> teacher.print_info()
lsl : 123459876

對應Python Console如下:

image-20211201195241710

Python 也支援一種多重繼承。 帶有多個基類的類定義語句如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-n>

派生類例項如果某一屬性在 DerivedClassName 中未找到,則會到 Base1 中搜尋它,然後(遞迴地)到 Base1 的基類中搜尋,如果在那裡未找到,再到 Base2 中搜尋,依此類推。

其他

推薦學習官方文件的迭代器生成器生成器表示式

結語

教程到這裡就結束了,最後推薦大家再去看看廖雪峰老師講解的異常處理IO的內容(也可以用到的時候再看),他比官網講解的更有條理。學完這些內容就基本入門了,今後可以根據自己應用的領域再進一步學習即可,比如深度學習、web開發等。