Skip to main content

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 應避免使用,可能會造成死循環。