2024年5月21日 星期二

Python 如何動態載入模組中的所有的檔案

Python 如何動態載入模組中的所有的檔案

寫了一個自動動態載入所有模組的代碼,免得每次加入新檔案都要在設置一次


這是測試的檔案結構

project/
│
├── utils/
│   ├── __init__.py
│   ├── upload.py
│   ├── info.py
│   └── download.py
└── main.py



目標是對 __init__.py 下手,他可以寫代碼動態載入同層資料夾中所有的模組

import os
import importlib
import inspect
from functools import wraps

# 設定警告訊息和資訊訊息的控制變數
print_warnings = False
print_info = False

# 強制只轉同名的函式或類別
force_promote = False

# 獲取當前目錄
module_dir = os.path.dirname(__file__)

# 獲取當前模組的包名
current_package = __package__

# 動態導入同層資料夾中的所有模組
for module_name in os.listdir(module_dir):
    if module_name.endswith('.py') and module_name != '__init__.py':
        module_name = module_name[:-3]
        try:
            # 若 globals 中已有相同名稱的模組且 print_warnings 為 True,則打印警告訊息
            if module_name in globals() and print_warnings:
                print(f"Warning: '{module_name}' already exists in globals and will be overridden.")

            # 動態導入模組
            module = importlib.import_module(f'.{module_name}', package=current_package)
            globals()[module_name] = module

            # 獲取模組中的屬性,排除內建屬性和非該模組定義的屬性
            module_attrs = [
                attr for attr in module.__dict__
                if not attr.startswith('__')
                and inspect.getmodule(getattr(module, attr)) == module
            ]
            # 找到與模組同名的屬性
            same_name_attr = [attr for attr in module_attrs if attr == module_name]

            # 強制只轉同名的函式或類別
            if force_promote:
                if same_name_attr:
                    # 若 globals 中已有相同名稱的屬性且 print_warnings 為 True,則打印警告訊息
                    if module_name in globals() and print_warnings:
                        print(f"Warning: '{module_name}' in globals will be overridden by '{module.__name__}.{same_name_attr[0]}'.")
                    globals()[module_name] = getattr(module, same_name_attr[0])
                else:
                    if print_warnings:
                        print(f"Warning: No attribute with the same name '{module_name}' found in module '{module.__name__}'.")
            else:
                # 當模組中只有一個與檔名同名的函式或類別時將其提升到父級命名空間
                if len(module_attrs) == 1 and module_attrs[0] == module_name:
                    # 若 globals 中已有相同名稱的屬性且 print_warnings 為 True,則打印警告訊息
                    if module_name in globals() and print_warnings:
                        print(f"Warning: '{module_name}' in globals will be overridden by '{module.__name__}.{module_attrs[0]}'.")
                    globals()[module_name] = getattr(module, module_attrs[0])

            # 若 print_info 為 True,則打印導入的模組及其屬性
            if print_info:
                imported_items = ", ".join(module_attrs)
                print(f"Module '{module_name}' imported with items: \n  [ {imported_items} ]")
        except Exception as e:
            # 打印導入模組時的錯誤訊息
            print(f"Error importing module '{module_name}': {e}")



然後在 main.py 中可以這樣引入

import utils

def main():
    # 調用子命名空間內的函式
    print("========================= utils.info =========================")
    utils.info()               # This will call the info function from utils.info

    # 調用子命名空間內的函式
    print("========================= utils.upload =========================")
    utils.upload.upload()      # This will call the upload function from utils.upload
    utils.upload.info()        # This will call the info from utils.upload
    # 調用類及其方法
    upload_instance = utils.upload.UploadClass()
    upload_instance.method()  # This will call the method from UploadClass in utils.upload

    print("========================= utils.download =========================")
    # 調用子命名空間內的函式
    utils.download.download()  # This will call the download function from utils.download
    utils.download.info()      # This will call the info from utils.download
    # 調用類及其方法
    download_instance = utils.download.DownloadClass()
    download_instance.method()  # This will call the method from DownloadClass in utils.download

if __name__ == "__main__":
    main()



再來是其他檔案 upload.py

def upload():
    print("This is the upload function from utils.upload")

def info():
    print("This is the info function from utils.upload")

class UploadClass:
    def method(self):
        print("This is a method from UploadClass in utils.upload")

下一個 download.py

def download():
    print("This is the download function from utils.download")

def info():
    print("This is the info function from utils.download")

class DownloadClass:
    def method(self):
        print("This is a method from DownloadClass in utils.download")


最後一個 info.py

def info():
    print("This is the info function from utils.info")

其中這個比較特別的我把他設計成
當模組中只有一個與檔名同名的函式或類別時將其提升到父級命名空間
所以可以省略重複的 info 只需要一次就可以了



其他檔案因為存在著第二個函式或類別就不會自動提升了,這是為了避免重名碰撞的問題
雖然無法完全避免,但是至少用當按當索引很容易就會不小心看到了,免得被自己埋坑了

唯一要注意的就是包名不要取的太過容易碰撞了,雖然 import utils 會優先讀取當前目錄,但要是沒有會繼續往環境庫裡面讀取的


最後還有一篇延伸的文章是
CHG: Python 如何動態轉發模組中的函式 (charlottehong.blogspot.com)

在這一篇我們已經做到如何自動加載所有檔案,再來利用裝飾器轉發模組中的所有函式

沒有留言:

張貼留言