解決python3中關於import的疑難雜症 python中import與包管理

python中import與包管理

概念:模塊與包

  • 模塊module:一般是以.py爲後綴的文件,也包括.pyo.pyc.pyd.so.dll後綴的文件,模塊內定義了函數、類以及變量
  • package:包是含有若干個模塊的文件夾,在工程項目用包管理模塊可以避免模塊名衝突

__init__.py

在Python工程項目中,如果一個文件夾下有__init__.py文件就會認爲該文件夾是一個包package,這樣可以方便組織工程文件,避免模塊名衝突。

  • __init__.py爲空時僅用於標識當前這個文件夾是一個包package

  • __all__變量指明當該包被import *時,哪些模塊module會被導入

  • 可以利用__init__.py對外提供類型、變量及接口,對用戶隱藏各個子模塊的實現細節

  • 當我們import一個包時,會自動加載該包對應的__init__.py,因此如果在其中做太複雜的運算會造成不必要的開銷

sys.modules

sys.modules維護了一個已加載module的字典,第二次加載該module時可以直接從字典中查找,加快執行速度。

import sys
print(sys.modules)

// 輸出:
{'random': <module 'random' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/random.pyc'>, 'subprocess': <module 'subprocess' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.pyc'>, 'sysconfig': <module 'sysconfig' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/sysconfig.pyc'>, 'gc': <module 'gc' (built-in)>}

namespace

  • local namespace:函數的命名空間,記錄函數的變量
  • global namespace:模塊的命名空間,記錄模塊的變量(函數、類、導入的模塊、模塊級別的變量和常量)
  • build-in namespace:包含build-in functionexceptions,可被任意模塊訪問

import方式影響我們使用包的方式正是namespace作用的體現:

from foo import bar  # 將模塊foo中的函數/變量bar導入到當前模塊的命名空間, 可以直接訪問bar
import foo  # 導入模塊foo同時保留它自己的命名空間, 需要通過foo.bar的方式來訪問bar

模塊內部屬性

  • __doc__:文件註釋
  • __file__:當前文件路徑
  • __package__:導入文件的路徑
  • __cached__:導入文件的緩存路徑
  • __name__:導入文件的路徑加文件名稱
  • __builtins__:包含內置函數

python內置模塊

  • os:提供文件和目錄等的系統級操作
  • sys:提供對解釋器相關的操作
  • hashlib:提供加密相關的操作,替代了md5sha模塊
  • shutil:提供文件、文件夾和壓縮包等處理模塊
  • configparser:提供對特定配置的操作
  • logging:提供日誌功能
  • timedatetime:提供時間相關操作
  • random:提供隨機數操作
  • jsonpickle:提供序列化操作
  • shelve:提供簡單kv將內存數據通過文件持久化的功能

import方式

1. 簡介

在Python中import的常用操作爲:

import somemodule  # 導入整個模塊
from somemodule import somefunction  # 從模塊中導入單個函數
from somemodule import firstfunc, secondfunc, thirdfunc  # 從模塊中導入多個函數
from somemodule import *  # 從模塊中導入所有函數

2. 執行import的步驟

  1. 創建一個新的module對象
  2. 將該module對象插入sys.modules
  3. 裝載module的代碼
  4. 執行新的module中對應的代碼

3. import的搜索包順序

注意第三步裝載module代碼時python解釋器需要先搜索到對應的.py文件,搜索順序爲:

  • sys.path:包含了當前腳本的路徑和其他查找包(系統庫、第三方庫等)的路徑,你也可以在代碼中通過sys.path.append()動態添加搜索路徑
  • PYTHONPATH
  • 查看默認路徑,比如Linux下爲/usr/local/lib/python/

4. 絕對導入與相對導入

絕對導入和相對導入的概念只針對於包內模塊導入包內模塊,注意如果foo.pybar.py在同一個非包(沒有__init__.py文件)的目錄下,那麼它們之間可以互相import,不存在絕對導入和相對導入的問題。

在Python3中建議使用絕對導入。

舉個例子:

$ tree
mypackage
├── __init__.py
├── module_bar.py
└── module_foo.py

在包mypackage內,如果module_bar要導入module_foo,那麼有三種方式:

# 方法一: 
import module_foo

# 方法二:
# 如果是上層文件夾寫.., 上上層文件夾寫..., 以此類推
from . import module_foo

# 方法三:
from mypackage import module_foo
import mypackage.module_foo
  • 對於python2而言,方法一和方法二都是相對導入,效果一樣,但是前者被稱爲隱式相對導入,後者被稱爲顯式相對導入,方法三是絕對導入(會在sys.path中的路徑搜索)

  • 對於python3而言,方法二是相對導入,方法一和方法三都是絕對導入,官方更推薦方法三

5. 包導入

包的導入和模塊導入基本一致,只不過導入包時會執行__init__.py。如果只是導入一個包import package而不指名任何模塊,且包中的__init__.py沒有其他的初始化操作,那麼包下面的模塊是無法被自動導入的。

6. 直接運行與模塊運行

以下面的項目爲例:

$ tree
.
└── mypackage
    ├── __init__.py
    └── module_foo.py
    
# module_foo.py內容如下:
import sys
print(sys.path)

我們有兩種方式運行module_foo.py

-m參數表示run library module as a script,即以腳本的方式執行模塊。

# 直接運行: 第一個目錄是模塊module_foo所在的
$ python3 -B mypackage/module_foo.py    
['/Users/didi/Desktop/MyProject/mypackage', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload', '/Users/didi/Library/Python/3.7/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages']

# 模塊運行: 第一個目錄是當前路徑
$ python3 -B -m mypackage.module_foo
['/Users/didi/Desktop/MyProject', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload', '/Users/didi/Library/Python/3.7/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages']

實例:包之間模塊引用的疑難雜症

1. 項目demo

假設當前你的工程文件目錄如下(僅針對python3):

注意這裏我的文件夾下並沒有__init__.py,嚴格來講它們並不是包,只是將聯繫緊密的模塊放在同一個文件夾下方便工程項目管理。

.
└── src
    ├── bar_package
    │   └── module_1.py
    ├── foo_package
    │   ├── module_2.py
    │   └── module_3.py
    └── main.py
# 注意
# 1) 所有模塊都以src爲根目錄, 包括main.py(當然這只是我個人習慣)
# 2) 引入方式都是絕對引入(python3推薦使用)

"""
module_1.py: 空文件
"""

"""
module_2.py: import同個包內的module_3
"""
from foo_package import module_3  # 引用同個包的模塊

"""
module_3.py: import另一個包內的module_1
"""
from bar_package import module_1  # 跨包引用模塊

if __name__ == "__main__":
    print("module_3 exec successfully!")

"""
main.py: import所有模塊
"""
from foo_package import module_3, module_2
from bar_package import module_1

上面就是通常項目文件包管理的方式,執行整個程序:

python3 -B src/main.py

2. 問題:單獨執行某個模塊

如果要單獨執行module_3.py,這時候會報錯:

$ python3 -B src/foo_package/module_3.py 
Traceback (most recent call last):
  File "src/foo_package/module_3.py", line 1, in <module>
    from bar_package import module_1  # 跨包引用模塊
ModuleNotFoundError: No module named 'bar_package'

回顧一下之前提到的import查找包的路徑,我們有兩種方法可以解決這個問題。

3. 方法一:通過模塊運行的方式解決(推薦)

本質上我們是希望將module_3.py這個模塊作爲腳本運行,所以我們可以帶上-m參數:

$ cd src  # 代碼中是以src爲根目錄的, 所以需要進入到src下
$ python3 -B -m foo_package.module_3   
module_3 exec successfully!

4. 方法二:在sys.path中添加查找路徑

前面的報錯是找不到bar_package的模塊名,因爲直接運行的話sys.path第一個路徑就是module_3.py的路徑,自然找不到它上層的bar_package,我們可以通過sys.path.append(..)將它的上層目錄也加入sys.path,修改後的module_3.py文件內容爲:

"""module_3.py
本質上就是將module_3.py的上級目錄加入到sys.path中, 這樣就可以找到bar_package了
"""
import os
import sys
parent_path = os.path.dirname(sys.path[0])
if parent_path not in sys.path:
    sys.path.append(parent_path)
from bar_package import module_1  # 跨包引用模塊


if __name__ == "__main__":
    print("module_3 exec successfully!")

需要注意的是,如果你使用的是如下這種寫法還是可能出現問題:

"""module_3.py
"""
import sys
sys.path.append("../")
from bar_package import module_1  # 跨包引用模塊


if __name__ == "__main__":
    print("module_3 exec successfully!")

# 進入到module_3.py所在的目錄, 輸出正常:
$ src/foo_package 
$ python3 -B module_3.py 
module_3 exec successfully!

# 直接在根目錄下執行會報錯:
$ python3 -B src/foo_package/module_3.py
Traceback (most recent call last):
  File "src/foo_package/module_3.py", line 3, in <module>
    from bar_package import module_1  # 跨包引用模塊
ModuleNotFoundError: No module named 'bar_package'

另一種簡潔的寫法是:

import sys
import oss
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))

5. 儘量不要使用相對引用

Python3不建議使用相對引用,最好遵循一定的開發規範,不要在代碼中混用絕對引用與相對引用。

Reference

[1] https://blog.csdn.net/weixin_38256474/article/details/81228492

[2] https://zhuanlan.zhihu.com/p/115350758

[3] https://www.cnblogs.com/schips/p/12148092.html

[4] https://www.jianshu.com/p/88b0f6f28f25

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章