跳至主要内容

Python Package and Module

Package

指的是放 Python 相關檔案的資料夾。
可簡單想成是一個存放 modules 與 packages 的資料夾。
資料夾中預期會有 __init__.py 檔案。

__init__.py 內容可為空,python3+ 中可省略(若考慮向下相容,建議可以保留此慣例)。

Package 資料夾可以槽串,同樣預期每個資料夾中都該有 __init__.py 檔案。
在 import 句子中使用時,須由 root package 依序寫清路徑全名(full path)。

Module

就是 package 資料夾內的 *.py 檔。
使用上需注意 module name 也是一種變量,所以使用時(命名時)須注意 shadow effect。

Module 的使用​

Module Importing 的概述​

與其他程使語言一樣,Python 執行環境中也會有一些基礎的 libs 可使用,
也就是所謂的 系統預設載入的 Modules,類似一般 SDK 中的相關 api。
當我們在進行開發時除了原生的 api 外,也可載入自行開發的工具或第三方所提供的工具來使用。
當載入不同 modules 或工具時則須注意載入的順序以及是否出現命名衝突的情形,
以避免載入錯誤或是無法成功在入所需 api 的情形發生。

Module Namespace 與路徑​

  • namespace 指的是模組所在的資料夾階層結構。
  • 路徑則是 Python Env 查找模組時的 namespace 描述方式。

絕對路徑(Absolute Path)

個人建議採用的方法,可以規避許多隱藏的風險。
由整個 Python Environment 來看的路徑。每個路徑都是唯一的。

相對路徑(Relative Path)

指的是 由 main process 角度來看 的路徑。並非當前使用中的 module 的角度來看。
個人不建議使用。

Import 語法使用建議​

  • 在考量 interpreter 工作方式下,建議優先採用 import [module] as [alias]

Priority of Importing Syntax

# 優先建議,by reference import (copy reference)
# import [module] as [alias]
import datetime as dt

# 下面可以當作沒看到

# 次要, 當載入成員是 物件類則 by reference, 是 primitive 則 by value (copy value)
# from [module] import [member] as [alias]
from datetime import date as dt

# 少用, 易發生變數名遮蔽. 搭配 __all__ 使用
# from [module] import *
from datetime import *

# 避用, 相對路徑會增加維護成本
# from [relative_path] import [module] as [alias]
from .. import datetime as dt

Import 相關語法​

import statement, by reference 來導入整個 module,
須以完整路徑(full path with namespace) 指到模組 (*.py)。
同 package 可用 shortcut path。

import 語法

import [namespace.module]

hello.py

def sayHelloWorld():
msg = 'Hello World'
print(msg)
return msg;


def say(msg: str):
print(msg)
return msg;

totem.math.py


pi = 3.14159

def add(a: int, b: int):
return a + b

run.py

import hello   #same package
import totem.math # diff package

hello.sayHelloWorld() # Hello World

hello.say("Good Day") # Good Day

sum = totem.math.add(10, 20)
print(sum) # 30

註: import 可導入的不單是 Python 檔,下列也在允許範圍

  • *.py
  • *.pyc
  • *.zip
  • *.dll
  • *.pyd
  • *.java

From Import 相關語法​

from...import... statement 用來導入模組中的特定成員。如變數(variable)、函數(function)、類別(class)、模組(module)等。
from import statement: by reference 導入物件類成員,
by value 來複製 primitive 類成員,

from... import 語法

run2.py

from [module with namespace] import [name1, name2, ...]
from hello import say as talk
from totem.math import pi as CONST_PI

talk("Yahoo~") # Yahoo~

print(CONST_PI) # 3.14159

from... import * 語法

不建議使用,相對上來講風險較高。容易發生命名衝突與成員覆寫。
from [module with namespace] import *

run3.py

  • 使用 asterisk 隨然可以方便一次匯入所有成員,但有可能不小心發生命名衝突。甚至覆蓋既有成員內容。
from hello import *
from totem.math import *

say("implicitly import by asterisk")

print(pi) #3.14159

pi = 3.14
print(pi) # 3.14, variable conflict

Module Reload(重新載入模組)​

reload 指的是在不中止 Python 程序的前提下,
重新載入相關模組(重新讀取文件的 source code)。
是 Python3+ 後出現的新功能。

reload 會重新執行頂層的 statement。
物件的部分需要等到重新使用重載變量時才會使用新載入的模組。

reload 語法

  • reload 函數在 imp 模組
from imp import reload
reload(module)

totem.helloworld.py

import time
print('Hello World')

current_timestamp = time.time()

def hello(name: str):
print(f'Hell, {name}', current_timestamp)

run4.py

from imp import reload
import totem.hellowrold

totem.hellowrold.hello("Totem")
totem.hellowrold.hello("Tim")
# Hello World
# Hell, Totem 1713194539.5623431
# Hell, Tim 1713194539.5623431 # 未 reload 時,先前取出的 timestamp 不變。

reload(totem.hellowrold)
totem.hellowrold.hello("Winnie")
totem.hellowrold.hello("Wendy")
# Hello World
# Hell, Winnie 1713194539.5633183 # reload 後,整個 source 重讀,也一併更新了 timestamp 。
# Hell, Wendy 1713194539.5633183

系統預設載入的 Modules​

sys.modules 變數

sys.modules: 可用來查詢 已載入的模組 資訊。 回傳結果為一個 dict,內容包含已導入模組名稱與 api 實際檔案位置。

import sys
print(sys.modules) # <class 'dict'>, 實際匯入檔清單

___ sys.path 變數___

sys.modules 可以列出已載入 modules 檔案位置,
而 sys.path 則是列出上述檔案所在的 資料夾 位置。

import sys
print(sys.path) # <class 'list'>, 實際匯入檔案所在資料夾

___ __pycache__ 資料夾___

Python 在導入 modules 時,會先將欲 導入的 modules 先執行一次 。
而過程中所產生的 byte code (*.pyc 檔)則會統一放在 __pycache__ 資料夾下。
也就是說 __pycache__: 可用來查詢已 Compiled 的程式碼(byte code)儲存位置相關資訊。
另外,執行時若原始 *.py 未做異動則不會重新 compile *.py 而是直接取用 *.pyc 檔,以提高效能。

Python Env(VM) 模組載入順序: Shadow effect​

Python 載入 Modules 時的查詢路徑

  • 有可能會依平台與 Python 版本而有所差異

  • Python 的 home directory: 程式碼所在位置

  • PYTHONPATH: class path 位置,也就是 OS 中的環境變數的設定。

  • Standard Library Directories: Python 安裝位置

  • *.py 檔案位置,特別指名資料夾中的 Python 檔。

  • 前三項設定預載入的 modules 資訊可以經由 sys.path 查詢。

Module 與封裝​

Python 沒有強制資料隱藏機制,但可利用語法慣例,不自動匯入特定模組。
這邊介紹 from... import * 語法 (Implicit importing) 時 module 的匯入規則。

Package 的使用​

Package 指的是放 Python 相關檔案的資料夾。
可簡單想成是存放 modules 與 packages 的資料夾。 且資料夾中預期(建議)會有 __init__.py 檔案。
__init__.py 內容可為空,python3+ 中可省略(若考慮向下相容,建議可以保留此慣例)。

Package 資料夾可以槽串,同樣預期每個資料夾中都該有 __init__.py 檔案。
在 import 句子中使用時,須由 root package 依序寫清路徑全名(full path)。

__init__.py

__init__.py 內容空的,或是撰寫下列相關資訊。

__all__: 是一個 array,所列的清單是 from m import * (implicitly)時允許被匯入的子模組清單, 換言之,implicitly import 時被列出的才能經由 import 匯入。是一種程式碼封裝技巧。
但仍需注意命名衝突與遮蔽問題,
遇到與 Local 變量(local defined var)重名時則不會匯入,改採用 local 變量。

此套件初始化時所需的準備工作(例如資料庫連線),以利後續使用。

註: _\init__.py 在首次匯入時會執行一次相關內容。

# __init__.py example

__all__ = ["echo", "surround", "reverse"]

___ __name__ 與 __main__ ___​

__name__ == '__main__'
__name__ == 'module_name'

當 module (*.py) 被直接執行時(如經過命令列執行),該 module 會變成主要執行緒,
此時,模組內建的 __name__ 變數會由預設的模組名稱改為 __main__ 字串。

而如果 module 是被引用(import),那麼__name__ 會是模組名稱。

因此部分的人在寫 臨時測試碼 時會利用此一特性,
也就是說當 module 直接執行時才會呼叫測試碼。
不過,我本身倒是認為,測試碼應該另闢資料夾管理才是。

___ __name__ 範例___

  • 當執行的是 Exam.py (包含 name, main)
print('Hello: This is Exam class')

class Exam:
# Python 的型別(冒號後面)僅只是 [提示]。所以應該自行檢查。
def __init__(self, name: str, score: int, penalty: int):
if not isinstance(name, str):
raise TypeError('name should be str')
self.score = score
self.penalty = penalty
self.name = name

def win(self):
self.score += 1

def lose(self):
self.penalty += 1

def get_points(self):
return self.score - self.penalty

def display(self):
return self.name + "-" + str(self.get_points())

if __name__ =='__main__':
print('Testers of Exam class')
exe1 = Exam("Tom", 80, 10)
exe2 = Exam("Jack", 95, 30)
exe3 = Exam("May", 60, 20)
exe1.win()
exe2.lose()

exams = [exe1.display(), exe2.display(), exe3.display()]
print(exams)

# Hello: This is Exam class
# Testers of Exam class
# ['Tom-71', 'Jack-64', 'May-40']
  • run.py
import Exam as ex

print(type(ex))
# <class 'module'>

# console:
# Hello: This is Exam class (這邊是載入時所執行)
# <class 'module'>

  • 當執行的是 Exam2.py (不包含 name, main)
print('Hello: This is Exam class')

class Exam:
# Python 的型別(冒號後面)僅只是 [提示]。所以應該自行檢查。
def __init__(self, name: str, score: int, penalty: int):
if not isinstance(name, str):
raise TypeError('name should be str')
self.score = score
self.penalty = penalty
self.name = name

def win(self):
self.score += 1

def lose(self):
self.penalty += 1

def get_points(self):
return self.score - self.penalty

def display(self):
return self.name + "-" + str(self.get_points())

print('Testers of Exam class')
exe1 = Exam("Tom", 80, 10)
exe2 = Exam("Jack", 95, 30)
exe3 = Exam("May", 60, 20)
exe1.win()
exe2.lose()

exams = [exe1.display(), exe2.display(), exe3.display()]
print(exams)
  • run.py
import Exam as ex2

print(type(ex2))
# <class 'module'>

# console:
# Hello: This is Exam class
# Testers of Exam class
# ['Tom-71', 'Jack-64', 'May-40'] (# 所有測試碼都會被執行)
# <class 'module'>

Module(*.py) 使用陷阱與注意事項​

雜七雜八注意事項

  • module name 也是一種變量,所以須注意命名衝突
  • Python Env 在載入 libs 時會先執行一次
  • 以 import 指令載入的程式碼是 by reference 使用
  • 以 from import 指令載入的程式碼物件類的是 by reference 使用,純值類的則時 by value。
  • implicitly import 需注意是否發生成員遮蔽情形
  • forward reference: 向前引用,Python interpreter 是依句子順序匯入與編譯。因此,無法使用後方定義變數,也無法理解後方匯入的模組。
  • 遞迴導入: 相互 import 的 modules 應避免使用,可能會造成死循環。