Python模組(module)

ThankCAT發表於2023-01-04

模組化(module)程式設計理念

模組和包概念的進化史

“量變引起質變”是哲學中一個重要的理論。量變為什麼會引起質變呢?本質上理解,隨著數量的增加,管理方式會發生本質的變化;舊的管理方式完全不適合,必須採用新的管理方式。

程式越來越複雜,語句多了,怎麼管理?很自然的,我們會將實現同一個功能的語句封裝到函式中,統一管理和呼叫,於是函式誕生了。

程式更加複雜,函式和變數多了,怎麼管理?同樣的思路,“物以類聚”,我們將同一型別物件的“資料和行為”,也就是“變數和函式”,放到一起統一管理和呼叫,於是“類和物件”誕生了。

程式繼續複雜,函式和類更加多了,怎麼辦?好,我們將實現類似功能的函式和類統統放到一個模組中,於是“模組”誕生了。

程式還要複雜,模組多了,怎麼辦? 於是,我們將實現類似功能的模組放到一起,於是“包”就誕生了。

標準庫模組(standard library)

與函式類似,模組也分為標準庫模組和使用者自定義模組。

Python 標準庫提供了作業系統功能、網路通訊、文字處理、檔案處理、數學運算等基本的功能。比如:random(隨機數)、math(數學運算)、time(時間處理)、file(檔案處理)、os(和作業系統互動)、sys(和直譯器互動)等。

為什麼需要模組化程式設計

模組(module)對應於 Python 原始碼檔案(.py 檔案)。模組中可以定義變數、函式、類、普通語句。 這樣,我們可以將一個 Python 程式分解成多個模組,便於後期的重複應用

模組化程式設計(Modular Programming)將一個任務分解成多個模組。每個模組就像一個積木一樣,便於後期的反覆使用、反覆搭建

模組化程式設計的流

模組化程式設計的一般流程:

  1. 設計 API,進行功能描述。
  2. 編碼實現 API 中描述的功能。
  3. 在模組中編寫測試程式碼,並消除全域性程式碼。
  4. 使用私有函式實現不被外部客戶端呼叫的模組函式。

模組的 API 和功能描述要點

API(Application Programming Interface 應用程式程式設計介面)是用於描述模組中提供的函式和類的功能描述和使用方式描述。

模組化程式設計中,首先設計的就是模組的 API(即要實現的功能描述),然後開始編碼實現 API 中描述的功能。最後,在其他模組中匯入本模組進行呼叫。

我們可以透過help(模組名)檢視模組的API。一般使用時先匯入模組 然後透過help函式檢視。

【示例】匯入 math 模組,並透過 help()檢視 math 模組的 API

import math

help(math)

""" Help on built-in module math:

NAME
    math

DESCRIPTION
    This module provides access to the mathematical functions
    defined by the C standard.

FUNCTIONS
    acos(x, /)
        Return the arc cosine (measured in radians) of x.

        The result is between 0 and pi.

    acosh(x, /)
        Return the inverse hyperbolic cosine of x.

    asin(x, /)
        Return the arc sine (measured in radians) of x.

        The result is between -pi/2 and pi/2.
-- More  -- """

【示例】設計計算薪水模組的 AP

"""
用於計算公司員工資訊的模組
"""
company = "WINCET"


def yesrsalary(monthsalary):
    """透過傳入的月薪,計算年薪"""
    return monthsalary * 12


def daysalary(monthsalary):
    """透過傳入的月薪,計算日新,法定月工作日22.5天"""
    return monthsalary / 22.5

我們可以透過__doc__可以獲得模組的文件字串的內容。

import salary

print(salary.__doc__)
print(salary.company)
print(salary.daysalary.__doc__)
print(salary.yesrsalary.__doc__)
print(salary.daysalary(12000))
print(salary.yesrsalary(12000))

""" 
用於計算公司員工資訊的模組

WINCET
透過傳入的月薪,計算日新,法定月工作日22.5天
透過傳入的月薪,計算年薪
533.3333333333334
144000 
"""

模組的建立和測試程式碼

每個模組都有一個名稱,透過特殊變數__name__可以獲取模組的名稱。在正常情況下,模組名字對應原始檔名。 僅有一個例外,就是當一個模組被作為程式入口時(主程式、互動式提示符下),它的__name__的值為“__main__”。我們可以根據這個特點,將模組原始碼檔案中的測試程式碼進行獨立的處理。例如:

import math
math.__name__  #輸出'math'

【示例】__name__ == "__main__獨立處理模組的測試程式碼

"""
用於計算公司員工資訊的模組
"""
company = "WINCET"


def yesrsalary(monthsalary):
    """透過傳入的月薪,計算年薪"""
    return monthsalary * 12


def daysalary(monthsalary):
    """透過傳入的月薪,計算日新,法定月工作日22.5天"""
    return monthsalary / 22.5


if __name__ == "__main__":
    print(yesrsalary(12000))
    print((daysalary(12000)))


""" 
144000
533.3333333333334
"""

模組的匯入

模組化設計的好處之一就是“程式碼複用性高”。寫好的模組可以被反覆呼叫,重複使用。模組的匯入就是“在本模組中使用其他模組”。

import 語句導

mport 語句的基本語法格式如下:
import 模組名 #匯入一個模組
import 模組 1,模組 2… #匯入多個模組
import 模組名 as 模組別名 #匯入模組並使用新名字

mport 載入的模組分為四個通用類別:

  1. 使用 python 編寫的程式碼(.py 檔案);
  2. 已被編譯為共享庫或 DLL 的 C 或 C++擴充套件;
  3. 包好一組模組的包
  4. 使用 C 編寫並連結到 python 直譯器的內建模組;

我們一般透過 import 語句實現模組的匯入和使用,import 本質上是使用了內建函式
__import__()

當我們透過 import 匯入一個模組時,python 直譯器進行執行,最終會生成一個物件,這個物件就代表了被載入的模組

import math
print(id(math))
print(type(math))
print(math.pi) #透過 math.成員名來訪問模組中的成
"""
31840800
<class 'module'>
"""

由上,我們可以看到 math 模組被載入後,實際會生成一個 module 類的物件,該物件被math 變數引用。我們可以透過 math 變數引用模組中所有的內容。

我們透過 import 匯入多個模組,本質上也是生成多個 module 類的物件而已。

有時候,我們也需要給模組起個別名,本質上,這個別名僅僅是新建立一個變數引用載入的模組物件而已。

import math as m
#import math
#m = math
print(m.sqrt(4)) #開方運算

from…import 匯入

Python 中可以使用 from…import 匯入模組中的成員。基本語法格式如下:

from 模組名 import 成員 1,成員 2

如果希望匯入一個模組中的所有成員,則可以採用如下方式:

from 模組名 import *

【注】儘量避免“from 模組名 import ”這種寫法。 它表示匯入模組中所有的不 是以下劃線(_)開頭的名字都匯入到當前位置。 但你不知道你匯入什麼名字,很有可能 會覆蓋掉你之前已經定義的名字.而且可讀性極其的差。一般生產環境中儘量避免使用, 學習時沒有關係。

【示例】使用 from…import 匯入模組指定的成員

from math import pi,sin
print(sin(pi/2)) #輸出 1.0

import 語句和 from...import 語句的區別

import 匯入的是模組。

from...import 匯入的是模組中的一個函式/一個類。

如果進行類比的話,import 匯入的是“檔案”,我們要使用該“檔案”下的內容,必 須前加“檔名稱”。from...import 匯入的是檔案下的“內容”,我們直接使用這 些“內容”即可,前面再也不需要加“檔名稱”了

我們自定義一個模組 calculator.py:

"""一個實現四則運算的計算器""" 
def add(a,b):
	return a+b

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

class MyNum():
	def print123(self):
		print(123)

我們在另一個模組 test.py 測試:

import calculator
a = calculator.add(30,40)
# add(100,200) #不加模組名無法識別
print(a)
from calculator import *
a = add(100,200) #無需模組名,可以直接引用裡面的函式/類
print(a)
b = MyNum()
b.print123()

包 package 的使用

包(package)的概念和結構

當一個專案中有很多個模組時,需要再進行組織。我們將功能類似的模組放到一起, 形成了“包”。本質上,“包”就是一個必須有__init__.py 的資料夾。典型結構如下:

包下面可以包含“模組(module)”,也可以再包含“子包(subpackage)”。就像檔案 夾下面可以有檔案,也可以有子資料夾一樣。

上圖中,a 是上層的包,下面有一個子包:aa。可以看到每個包裡面都有__init__.py 檔案。

匯入包操作和本質

上一節中的包結構,我們需要匯入 module_AA.py。方式如下:

import a.aa.module_AA

在使用時,必須加完整名稱來引用,比如:a.aa.module_AA.fun_AA()

from a.aa import module_AA

在使用時,直接可以使用模組名。 比如:module_AA.fun_AA()

from a.aa.module_AA import fun_AA # 直接匯入函式

在使用時,直接可以使用函式名。 比如:fun_AA()

**【注】 **

  1. from package import item 這種語法中,item 可以是包、模組,也可以是函式、 類、變數。

  2. import item1.item2 這種語法中,item 必須是包或模組,不能是其他。

匯入包的本質其實是“匯入了包的__init__.py”檔案。也就是說,”import pack1”意味 著執行了包 pack1 下面的__init__.py 檔案。 這樣,可以在__init__.py 中批次匯入我們需要 的模組,而不再需要一個個匯入。

__init__.py 的三個核心作用:

  1. 作為包的標識,不能刪除。

  2. 用來實現模糊匯入

  3. 匯入包實質是執行__init__.py 檔案,可以在__init__.py 檔案中做這個包的初始化、以及 需要統一執行程式碼、批次匯入。

【示例】測試包的__init__.py 檔案本質用法

a 包下的__init__.py 檔案內容:

import turtle
import math
print("匯入 a 包")

b 包下的 module_B1.py 檔案中匯入 a 包,程式碼如下:

import a
print(a.math.pi)
"""
匯入 a 包
3.141592653589793
"""

【注】如上測試我們可以看出 python 的設計者非常巧妙的透過__init__.py 檔案將包轉成了 模組的操作。因此,可以說“包的本質還是模組”。

用*匯入包

import * 這樣的語句理論上是希望檔案系統找出包中所有的子模組,然後匯入它們。 這可能會花長時間等。Python 解決方案是提供一個明確的包索引。

這個索引由 __init__.py 定義 __all__ 變數,該變數為一列表,如上例 a 包下的 __init__.py 中,可定義 __all__ = ["module_A","module_A2"] 這意味著, from sound.effects import * 會從對應的包中匯入以上兩個子模組;

__all__ = ["module_A", "module_B"]

包內引用

如果是子包內的引用,可以按相對位置引入子模組 以 aa 包下的 module_AA 中匯入 a 包下內容為例:

from .. import module_A # ..表示上級目錄 .表示同級目錄
from . import module_A2 # .表示同級目錄

sys.path 和模組搜尋路徑

當我們匯入某個模組檔案時, Python 直譯器去哪裡找這個檔案呢?只有找到這個文 件才能讀取、裝載執行該模組檔案。它一般按照如下路徑尋找模組檔案(按照順序尋找,找 到即停不繼續往下尋找):

  1. 置模組

  2. 當前目錄

  3. 程式的主目錄

  4. pythonpath 目錄(如果已經設定了 pythonpath 環境變數)

  5. 標準連結庫目錄

  6. 第三方庫目錄(site-packages 目錄)

  7. .pth 檔案的內容(如果存在的話)

  8. sys.path.append()臨時新增的目錄

當任何一個 python 程式啟動時,就將上面這些搜尋路徑(除內建模組以外的路徑)進行收集, 放到 sys 模組的 path 屬性中(sys.path)

使用 sys.path 檢視和臨時修改搜尋路徑

import sys
sys.path.append("d:/")
print(sys.path)

模組釋出和安裝

模組釋出

當我們完成了某個模組開發後,可以將他對外發布,其他開發者也可以以“第三方擴充套件 庫”的方式使用我們的模組。我們按照如下步驟即可實現模組的釋出:

  1. 為模組檔案建立如下結構的資料夾(一般,資料夾的名字和模組的名字一樣):

  1. 在資料夾中建立一個名為『setup.py』的檔案,內容如下:

    from distutils.core import setup
    
    setup(
        name="supermath",  # 對外模組名
        version="1.0",  # 版本號
        description="這是一個對發發布的測試模組,Test,Not use",  # 描述
        author="HoveyCHEN",  # 作者
        author_email="chenhao852@icloud.com",  # 作者郵箱
        py_modules=["supermath.demo1_add", "supermath.demo2_mul"]  # 要釋出的模組
    )
    
  2. 建一個釋出檔案。透過終端,cd 到模組資料夾 c 下面,再鍵入命令:

    python setup.py sdist
    
  3. 執行結果

    # chenh @ HAO-PC-ROG in ~\OneDrive\Data Learn\Python 基礎\課堂筆記\12\C [20:13:37]
    $ python setup.py sdist
    running sdist
    running egg_info
    creating supermath.egg-info
    writing supermath.egg-info\PKG-INFO
    writing dependency_links to supermath.egg-info\dependency_links.txt
    writing top-level names to supermath.egg-info\top_level.txt
    writing manifest file 'supermath.egg-info\SOURCES.txt'
    reading manifest file 'supermath.egg-info\SOURCES.txt'
    writing manifest file 'supermath.egg-info\SOURCES.txt'
    warning: sdist: standard file not found: should have one of README, README.rst, README.txt, README.md
    
    running check
    creating supermath-1.0
    creating supermath-1.0\supermath
    creating supermath-1.0\supermath.egg-info
    copying files to supermath-1.0...
    copying setup.py -> supermath-1.0
    copying supermath\__init__.py -> supermath-1.0\supermath
    copying supermath\demo1_add.py -> supermath-1.0\supermath
    copying supermath\demo2_mul.py -> supermath-1.0\supermath
    copying supermath.egg-info\PKG-INFO -> supermath-1.0\supermath.egg-info
    copying supermath.egg-info\SOURCES.txt -> supermath-1.0\supermath.egg-info
    copying supermath.egg-info\dependency_links.txt -> supermath-1.0\supermath.egg-info
    copying supermath.egg-info\top_level.txt -> supermath-1.0\supermath.egg-info
    Writing supermath-1.0\setup.cfg
    creating dist
    Creating tar archive
    removing 'supermath-1.0' (and everything under it)
    
  4. 執行完畢後,目錄結構變為:(supermath-1.0.tar.gz 就是我們的包)

demo1_add.py

def add(a, b):
    """這是一個加法方法"""
    return a + b

demo2_mul.py

def mul(a, b):
    """這是一個乘法模組"""
    return a * b

模組安裝

將釋出安裝到你的本地計算機上。仍在 cmd 命令列模式下操作,進 setup.py 所在目 錄,鍵入命令:

python setup.py install

執行結果:

# chenh @ HAO-PC-ROG in ~\OneDrive\Data Learn\Python 基礎\課堂筆記\12\C [20:19:29]
$ python setup.py install
running install
C:\Users\chenh\AppData\Local\Programs\Python\Python311\Lib\site-packages\setuptools\command\install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.
  warnings.warn(
C:\Users\chenh\AppData\Local\Programs\Python\Python311\Lib\site-packages\setuptools\command\easy_install.py:144: EasyInstallDeprecationWarning: easy_install command is deprecated. Use build and pip and other standards-based tools.
  warnings.warn(
running bdist_egg
running egg_info
writing supermath.egg-info\PKG-INFO
writing dependency_links to supermath.egg-info\dependency_links.txt
writing top-level names to supermath.egg-info\top_level.txt
reading manifest file 'supermath.egg-info\SOURCES.txt'
writing manifest file 'supermath.egg-info\SOURCES.txt'
installing library code to build\bdist.win-amd64\egg
running install_lib
running build_py
creating build
creating build\lib
creating build\lib\supermath
copying supermath\__init__.py -> build\lib\supermath
copying supermath\demo1_add.py -> build\lib\supermath
copying supermath\demo2_mul.py -> build\lib\supermath
creating build\bdist.win-amd64
creating build\bdist.win-amd64\egg
creating build\bdist.win-amd64\egg\supermath
copying build\lib\supermath\demo1_add.py -> build\bdist.win-amd64\egg\supermath
copying build\lib\supermath\demo2_mul.py -> build\bdist.win-amd64\egg\supermath
copying build\lib\supermath\__init__.py -> build\bdist.win-amd64\egg\supermath
byte-compiling build\bdist.win-amd64\egg\supermath\demo1_add.py to demo1_add.cpython-311.pyc
byte-compiling build\bdist.win-amd64\egg\supermath\demo2_mul.py to demo2_mul.cpython-311.pyc
byte-compiling build\bdist.win-amd64\egg\supermath\__init__.py to __init__.cpython-311.pyc
creating build\bdist.win-amd64\egg\EGG-INFO
copying supermath.egg-info\PKG-INFO -> build\bdist.win-amd64\egg\EGG-INFO
copying supermath.egg-info\SOURCES.txt -> build\bdist.win-amd64\egg\EGG-INFO
copying supermath.egg-info\dependency_links.txt -> build\bdist.win-amd64\egg\EGG-INFO
copying supermath.egg-info\top_level.txt -> build\bdist.win-amd64\egg\EGG-INFO
zip_safe flag not set; analyzing archive contents...
creating 'dist\supermath-1.0-py3.11.egg' and adding 'build\bdist.win-amd64\egg' to it
removing 'build\bdist.win-amd64\egg' (and everything under it)
Processing supermath-1.0-py3.11.egg
Copying supermath-1.0-py3.11.egg to c:\users\chenh\appdata\local\programs\python\python311\lib\site-packages
Adding supermath 1.0 to easy-install.pth file

Installed c:\users\chenh\appdata\local\programs\python\python311\lib\site-packages\supermath-1.0-py3.11.egg
Processing dependencies for supermath==1.0
Finished processing dependencies for supermath==1.0

【測試】測試我們安裝的模組

from supermath.demo1_add import add
from supermath.demo2_mul import mul

print(add(10, 2))   # 12
print(mul(10, 2))   # 20
print(add.__doc__)  # 這是一個加法方法
print(mul.__doc__)  # 這是一個乘法模組

相關文章