1. 程式人生 > >python 歷險記(五)— python 中的模組

python 歷險記(五)— python 中的模組

目錄

前言

這次我們繼續探險,來搞定 python 中的模組(module)。兵馬未動,糧草先行,開工之前先看看基礎是否補齊了^_^。

基礎

模組的概念你一定不會陌生吧,這是一個非常寬泛的概念,在各行各業都會用到。這裡我們涉及的只是軟體中的模組概念。說到模組,就得先了解下模組化程式設計的概念。(如果您對模組化程式設計的概念已經爛熟於心,儘可以略過,而直接跳到下一節)

模組化程式設計

模組化程式設計是指進行程式設計時將一個大程式按照功能劃分為若干小程式模組,每個小程式模組完成一個確定的功能,並且在這些模組之間建立必要的聯絡,通過模組的相互協作完成整個功能的程式設計方法。

——百度百科

舉個例子,假如我們要造一輛汽車,就可以先將汽車分成四個部件:發動機、底盤、電氣裝置、車身。每個部件都是一個模組,都會完成自己專屬的功能,同時也預留了和其他部件配合的介面。當這幾部分建造完成時,就可以組合在一起,從而完成了整個汽車的建造。

類比一下,汽車就是整個程式,而像發動機,底盤等就是程式中各個小的模組。只有當各個模組正常工作,並且暴露的介面和其他模組的介面完全契合,才能組合成一個完整且正常工作的程式。

模組化有哪些好處?

當然,如果不將程式分解成一個個獨立的部分,而是整個一大坨,也能夠完成所要的功能。那麼為什麼教科書還有實際使用中都會提倡模組化程式設計?這樣做有什麼好處呢?

  1. 控制程式設計的複雜度

    不知你看過《程式碼大全》沒有,裡面有一句非常著名的格言:軟體的首要使命就是管理複雜度。完整的軟體功能複雜度是非常高的,如果不使用有效的方法加以管理,很可能會陷入複雜的泥潭中不可自拔。而將程式分解成模組,則會將整體功能的複雜度有效的下分到各個模組中。每個模組只要能夠管理好自己的複雜度就可以了。

  2. 提高程式碼的重用性

    還是以造汽車為例,假設我造了一個很牛的發動機,多款車型都可以使用它。程式設計也一樣,如果一個模組能夠完成特定的功能,且與父程式耦合度較小,多個程式都可以使用它。

  3. 易於維護和擴充套件

    小 A 寫了一個程式,並將各個部分劃分的非常明確,再加以人性化的函式命名和註釋。即使有一天小 A 離職了,小 B 要接過來維護以及在此基礎上再開發新的功能也不難。

既然模組化就這麼多好處, 強大的 python 當然也會吸收這個優秀的設計思想,並且在語言中有所體現,那就是 python 的模組(module)。

什麼是 python 中的模組?

先來看一個示例:

  1. 建立 python 檔案 a.py,並在檔案中定義函式 sum

    def sum(a, b):
        return a + b
  2. 建立 python 檔案 b.py, 並呼叫 sum 函式

    from a import sum
    
    print(sum(1, 2)) # 3

檔案 a.py 就是一個模組(module),b.py就是一個主模組(main module)。

在 b.py 中有這麼一句 from a import sum ,是指將模組 a 中的 sum 函式匯入到當前模組中。我們定義的檔名是 a.py ,而模組名就是去掉字尾後得到的 模組 a。那麼能不能再多匯入幾個函式或者匯入模組 a 的全部函式呢?當然可以,這個我們後面講。

呼叫模組時,通過檔名就可以確定模組的名字,那麼在模組(module)內部,能知道自己姓甚名誰嗎?還真能。

每個模組都有一個全域性變數 __name__ ,它就是模組的名字。上面 a.py 的內容不變,修改下 b.py 的內容。

import a

print(a.__name__)  # a
print(a.sum(1, 2))  # 3

來,一起總結下:

  1. python 模組(module) 是指包含 python 定義(包括 類,函式,變數)和語句的檔案(.py做字尾)
  2. 模組名就是模組檔名稱去掉.py 字尾
  3. 在模組內部,可以通過全域性變數 __name__ 得到模組名稱

引入模組有幾種方式?

要匯入模組並呼叫,前提要匯入的 python 模組中有料(函式,變數,class)才可以。先來定義一個 python 模組 calc

def plus(a, b):
    return a + b


def subtract(a, b):
    return a - b

再建立一個 main.py 檔案,在其中做引入操作。okay,準備好了,那我們來逐個看下可以引入模組的方式吧。

  • 引入整個模組,呼叫時需要加上模組名

    import calc
    
    print(calc.plus(1, 2)) # 3
    
    print(calc.subtract(2, 1)) # 1
  • 引入模組特定的函式或變數,呼叫時無需加模組名

    from calc import plus, subtract
    
    print(plus(1, 2))  # 3
    
    print(subtract(2, 1))  # 1
  • 引入整個模組,呼叫時無需加上模組名

    from calc import *
    
    print(plus(1, 2))  # 3
    
    print(subtract(2, 1))  # 1
  • 引入整個模組,並對模組重新命名,呼叫時加上重新命名後的模組名

    import calc as calculator
    
    print(calculator.plus(1, 2))  # 3
    
    print(calculator.subtract(2, 1))  # 1
  • 引入模組特定的函式或變數,並對其重新命名,呼叫時無需加模組名

    from calc import plus as add, subtract as sub
    
    print(add(1, 2))  # 3
    
    print(sub(2, 1))  # 1

    數一下,一共是 6 種方式,歸納一下就是 from , import , as , * 這些符號的組合而已。

模組的查詢順序

在上幾篇文章中已經用瞭如 osshutilsjson 等多個模組 ,這些模組都是 python 的內建模組。相比之下,我們剛才使用的 calc 模組就是自定義模組。

假設我們使用 import calc 匯入 calc 模組, python 在啟動時按照什麼樣的順序來查詢這個模組呢?

  1. 先查詢內建(built-in)模組中有沒有,如果沒有轉到 2
  2. 查詢 sys.path 變數指定的路徑下有沒有, 有的話就使用,沒有就報錯

sys.path 變數中儲存了那些路徑呢?

  1. 當前執行的 python 指令碼所在的目錄

  2. 環境變數 PYTHONPATH 中的路徑,它和 shell 環境變數 PATH 差不多

    這個變數可以使用 python 指令碼在執行時修改它

  3. 預設的 python 安裝包的路徑

想要看下你的電腦當前 sys.path 有哪些路徑嗎?執行下面程式碼就可以

import sys
print(sys.path)

查詢模組的順序是從前向後,只要查到就使用,因此這個變數儲存路徑的順序很重要。

模組中包含執行語句的情況

如果引入的模組中包含一些執行語句,那麼在匯入模組時這些語句就會執行。但是即使同樣的模組被匯入了兩次,這些語句也只能執行一次。

來看下面的例子, 定義 calc 模組

print('I am clac module')


def plus(a, b):
    return a + b


def subtract(a, b):
    return a - b

並且在 main.py 中定義匯入兩次 calc 模組的函式

from calc import plus
from calc import subtract


print(plus(1, 2))
print(subtract(1, 2))

結果是 'I am clac module' 只會被列印一次。

用 dir() 函式來窺探模組

dir() 函式是 python 的內建函式,可用來獲取模組的屬性,方法等資訊,當我們剛接觸一個模組,不清楚它由哪些有用的屬性和方法時,就可以用 dir() 來一探究竟。

以常用的 json模組 為例,我們來展示下它的屬性和方法

import json

print(dir(json))
# ['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', '__all__', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_default_decoder', '_default_encoder', 'codecs', 'decoder', 'detect_encoding', 'dump', 'dumps', 'encoder', 'load', 'loads', 'scanner']

其中以雙下劃線開頭的變數,如 __name__ 並非是模組自己定義的,而是與模組相關的預設屬性。

如果我想檢視當前模組內的所有屬性和方法呢?去掉 dir() 函式的引數就可以。拿上節的程式碼為例來看下。

from calc import plus
from calc import subtract


print(plus(1, 2))
print(subtract(1, 2))
print(dir())
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'plus', 'subtract']

我們會看到 calc 模組的 plus 和 substract 方法也展示了出來,那麼 dir 函式究竟是從哪裡獲取的資料,背後的機理是什麼呢?

其實每個模組內部都有一個子集的私有符號表,它就是模組內所有函式和方法共享的全域性符號表。當模組 B 匯入模組 A 時,就會把要匯入的模組 A 或者特定的方法,屬性放置到模組 B 的全域性符號表中,dir() 函式也就是從模組中的全域性符號表中獲取出的值。

python 的內建模組有哪些?

python 的內建模組太豐富了,幾乎可以滿足我們日常的任何需求。既然有輪子就在那裡,而且這輪子又快又好,又何必再造輪子呢?快來看下它的常用內建模組有哪些。

模組名 功能簡述
calendar 與日期相關
datetime 處理日期和時間
csv 讀寫 csv 檔案
json 讀寫 json 格式資料
collections 提供有用的資料結構
io 處理 I/O 流
os 基本的作業系統函式訪問
shutil 高階檔案處理
tempfile 建立臨時檔案和目錄
logging 日誌功能
random 生成偽隨機數
copy 複製資料相關
codec 編解碼
re 正則表示式
uuid 全域性唯一識別符號 (UUID)
multiprocessing 執行多個子程序
threading 執行緒
concurrent 非同步
argparse 解析命令列引數
atexit 註冊在程式退出時呼叫的函式
signal 處理 POSIX 訊號

光常用的模組就這麼一大堆,確實是很難都記住,記不住也沒關係,當需要用到的時候隨用隨查就可以了。