python import 詳解

當前目錄 和 腳本目錄

參考資料:https://techibee.com/python/get-current-directory-using-python/2790
current directory/working directory(工作目錄):運行python時用戶所在的目錄,就是用戶打shell命令的pwd看到的目錄,與python腳本在哪里無關(比如腳本文件在../../script.py)
os.getcwd():可以獲得current dir
工作目錄的作用是:讀取文件時,open('filename')指的是工作目錄下的filename文件,open('../filename')指的就是在工作目錄上一層的filename文件。

入口文件及所在目錄:
__file__: 腳本文件路徑
則腳本目錄可以這樣獲得:os.path.dirname(os.path.realpath(__file__))

導入詳解

參考資料:https://chrisyeh96.github.io/2017/08/08/definitive-guide-python-imports.html

python的import一直對我是個黑洞,我只是大致了解sys.path的功能。但是python的導入實際上是有很多學問的,弄懂它對開發運維都有巨大幫助, 我從上面這位斯坦福大學大二學生的教程中受益匪淺,決定把它編譯下來,順帶改正幾處他的筆誤。讀懂了上文,只要你不準備寫第三方庫,那么基本能夠應付幾乎所有的導入問題了。

關鍵點:

  1. import 語句根據sys.path的路徑進行搜索
  2. sys.path自動包含入口文件所在目錄,而不是包含working directory
  3. import一個package概念上就是導入該package的__init__.py文件

基本概念

  • module: *.py文件,module的名字為該文件的名字
  • built-in module: 被編譯進python解釋器的模塊(用C實現),因此沒有對應的*.py文件
  • package: 包含__init__.py的目錄,package的名字是目錄的名字
    • python 3.3以后,任何目錄都是package(即使沒有__init__.py)

例子的目錄結構

test/                      # root folder
    packA/                 # package packA
        subA/              # subpackage subA
            __init__.py
            sa1.py
            sa2.py
        __init__.py
        a1.py
        a2.py
    packB/                 # package packB (implicit namespace package)
        b1.py
        b2.py
    time.py
    random.py
    other.py
    start.py

import做了什么?

當import module時,python會運行其中的代碼;當import package時,python會運行__init__.py中的代碼

import的搜索路徑

import spam時,先搜索built-in module中是否有spam模塊,如果沒有則按順序搜索 sys.path下的是由有spam模塊。

假設運行>> python script.pysys.path的路徑列表是這樣初始化的的:

  • 包含入口文件script.py的目錄 (如果沒有指定script.py,直接>> python, 則是當前目錄).
  • PYTHONPATH (目錄列表,格式同shell變量PATH)
  • 默認的package包(如標注庫)

初始化完成后,python程序可以更改sys.path. 此外因為入口文件所在目錄排在標準庫之前,因此與標準庫同名的在該目錄下的自定義文件會替代標準庫被優先導入。 Source: Python 2 and 3.

要注意的是python解釋器會優先從built-in module中搜索module,找不到再從sys.path中搜索。built-in module可由命令sys.builtin_module_names獲得,如
sys, time這些模塊都是built-in module. (注意built-in modulebuilt-in function有區別, built-in function可以在builtins module找到,而builtins本身又是built-in module.)

如上面的目錄結構中:timebuilt-in module, 而random是 standard module, 因此在start.py文件中import time會導入的python的內置module,但是 import random會導入我們自定義的module。

sys.path注意點

最后強調一遍:當運行一個python腳本時,sys.path不關心工作目錄(當前目錄)在哪里,只關心入口文件所在的目錄在哪里。例如:如果當前目錄在test/下,執行python ./packA/subA/subA1.py, sys.path[0]test/packA/subA/而不是test/

此外sys.path是導入模塊所共享的。例如假設我們執行python start.py, 而start.py中執行了import packA.a1, 在a1中打印sys.path, 則它包含test/目錄(入口文件start.py的路徑), 而不是test/packA/(a1,py的路徑); 因此在a1.py中導入other應該執行import other ,因為other.py在搜索路徑test/中。

__init__.py詳解

__init__.py有以下2個功能:

  1. 將目錄轉化為可導入的package(python3.3及其之前)
  2. 執行package的初始化代碼

將目錄轉化為可導入的package

要導入不在入口文件所在路徑的module或package,該module需要在package中。

而package在python3.3之前都需要包含一個__init__.py文件,該文件可以為空。比如用python2.7運行start.py,它可以import packA, 但是不能import packB, 因為在 test/packB中不存在__init__.py

不過在python3.3及其以后的版本,所有目錄都被認為是package。假如用pythno3.6運行以下命令,輸入如下:

>>> import packB
>>> packB
<module 'packB' (namespace)>

運行package的初始化代碼

每當import package時,python會先執行__init__.py中的代碼。任何在__init__.py中定義的對象或文件,都在該包的namspace中。
例如:
test/packA/a1.py

def a1_func():
    print("running a1_func()")

test/packA/__init__.py

## this import makes a1_func directly accessible from packA.a1_func
from packA.a1 import a1_func

def packA_func():
    print("running packA_func()")

test/start.py

import packA  # "import packA.a1" will work just the same

packA.packA_func()
packA.a1_func()
packA.a1.a1_func()

python start.py輸出為:

running packA_func()
running a1_func()
running a1_func()

導入package

導入包含__inti__.py的package概念上等效于導入__init__.py作為一個模塊,這確實也是python的處理方式,從以下輸出可以看出來:

>>> import packA
>>> packA
<module 'packA' from 'packA\__init__.py'>

absolute導入與relative導入:重點

absolute導入:使用全路徑導入(從入口文件所在位置算起)
relative導入:以當前模塊(即腳本)作為相對位置來導入
其中relative導入分為2種:

  • explicit relative imports: 以from .<module/package> import X形式的導入,前面的點表示當前目錄,兩個點表示上一層目錄。。。
  • implicit relative import: 就好像當前目錄在sys.path中的導入,它只適用于python 2python3中不再支持:

The only acceptable syntax for relative imports is from .[module] import name. All import forms not starting with . are interpreted as absolute imports.

Source: What’s New in Python 3.0

例子如下:
假設運行入口文件是start.py, 它導入了a1, 而a1又導入了other, a1和sa1, 則a1中的導入方式可以這樣寫:

  • absolute imports:
import other
import packA.a2
import packA.subA.sa1
  • explict relative imports:
import other  # absolute imports
from . import a2 # explict relative imports
from .subA import sa1  # explict relative imports
  • implicit relative imports(python3不支持)
import other  # absolute imports
import a2  # implicit relative imports
import subA.sa1  # implicit relative imports

需要注意的是, .只能上溯至入口文件所在目錄(但不包括),因此from .. import other是不支持的,會報:ValueError: attempted relative import beyond top-level package。 因此只能用absolute import。

建議只使用absolute import , 不僅是因為明晰易懂,還因為相對導入的文件都無法直接運行,而絕對導入的module可以通過某種方式運行:將原來的入口文件所在目錄添加到sys.path中,下文將做詳細分析。

案例

Case1 不修改sys.path:

python start.py時,sys.path總包含test/目錄,導入sa1.py中的helloWorld函數, 使用絕對路徑導入:
from packA.subA.sa1 import helloWorld

Case2 可以修改sys.path:

假設start.py中導入了a2, a2中導入了sa2start.py永遠需要直接運行;不過我們有時候也希望能夠直接運行a2
但是問題是,當我們運行start.py時,sys.path中包含的目錄是test/,但是當我們運行a2時,sys.path中包含的目錄是test/packA/
當我們直接運行start.py時,在a2中要導入sa2, 導入語句是from PackA.subA import sa2; 但是直接運行a2時,上述導入方式就會報錯,因為test/不在搜索路徑中了,必須這樣導入: from subA import sa2. 不過這樣的導入語句如果運行start.py時又會報錯(Python3),因為test/packA不在搜索路徑中(不過python2的implicit relative import不報錯,不過我們今后基本不再用python2, 而且根據python之禪:盡量使用唯一的最好方式,最好使用絕對導入)。
總結一下:

Run from packA.subA import sa2 from subA import sa2
start.py OK Py2 OK, Py3 fail (subA not in test/)
a2.py fail (packA not in test/packA/) OK

從上表看出,a2無論哪種導入sa2的方式,要么運行start.py時報錯,要么運行a2時報錯,沒有同時都可以運行成功的方案。
下面提供了3種方案:

  1. 使用from packA.subA import sa2(中間一列),此時start.py當然沒問題; 將命令行切換到test/目錄下,運行python -m packA.a2,就等于直接運行a2了。
  2. 使用from packA.subA import sa2(中間一列),此時start.py當然沒問題;我們可以在運行a2前更改sys.path,將test加入搜索路徑, 這樣直接運行a2也OK了。
# a2.py
import os, sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))

# now this works, even when a2.py is run directly
from packA.subA import sa2

需要注意的是,如果python沒有配置好,__file__有時候不準確,可以使用 built-in inspect包來解決(this StackOverflow answer)。

  1. 使用第三列的寫法,可是只對python2有效
  2. 使用from packA.subA import sa2(中間一列),同時將test/加入到PYTHONPATH環境變量。

根據python之禪,我推薦使用第二種方案。

There should be one-- and preferably only one --obvious way to do it.

Case3

a2還是要導入sa2, 但這次我想直接運行a1,而非a2(start.py當然也要直接運行);則仍然可以用2,3,4的方案,但方案1不再起作用。

Case4:導入父級目錄的module

例如,想直接運行sa1, 而sa1想導入a1, 此時只能通過修改sys.pathPYTHONPATH來做到,就是用方案2, 4。
但是我建議:寫代碼時,盡量不要導入父級目錄的腳本。

其實pycharm的導入方式,就是修改了PYTHONPATHsys.path,不過它設置的地方比較多,細節待整理:

image1.png

image2.png

image3.png

python2與python3的區別

  1. python2支持implicit relative import, python3不支持
  2. python2的package要包含__init__.py,python3.3和更高版本把所有目錄都認為是package(implicit namespace packages)
  3. from <module> import *語法在python2中可以寫在函數中,python3只允許寫在module一級。

其他散落的知識點

  1. __init__.py中使用__all__
  • documentation for Python 2 and 3
  1. pip install -e <project>將project的root目錄加入sys.path
  1. from <module> import *不會將'_'開頭的名稱導入
  • documentation for Python 2 and 3
  1. 使用if __name__ == '__main__'檢測腳本是直接運行還是被導入的
  • documentation for Python 2 and 3

總結

對于一般的開發工作,上文介紹python的導入機制已經足夠用了。
但是如果你想開發package并且發布出去,那么再深入研究一番也是必須的。事實上,本文有大量知識點都有待深入挖掘,此外我覺得還可以研究一下pkgutilimportlib標準庫模塊。

補充:

寫此文已過去2個月,我在import logging的時候又遇到了新的問題:

import logging
logging.config.fileConfig('logging.ini')

上文報AttributeError: module 'logging' has no attribute 'config'.
而我這樣導入就沒問題, 而且可以直接使用loggin.getLogger:

import logging.config
logging.config.fileConfig('logging.ini')
logger = logging.getLogger(__name__)

其實看一下logging模塊就知道了:

logging $tree
.
├── __init__.py
├── __pycache__
│   ├── __init__.cpython-37.opt-1.pyc
│   ├── __init__.cpython-37.pyc
│   ├── config.cpython-37.opt-1.pyc
│   ├── config.cpython-37.pyc
│   ├── handlers.cpython-37.opt-1.pyc
│   └── handlers.cpython-37.pyc
├── config.py
└── handlers.py

__init__.py文件如下:

image.png

config是一個單獨的模塊,即使導入了logging,該模塊也未導入,因此不能使用;而getLogger定義在__init__.py中,導入logging.config的時候也導入了logging,因此__init__.py中的任何函數都可以直接使用。
如果只是使用getLogger,只導入logging而不導入logging.config也是可以的:

import logging

logger = logging.getLogger(__name__)

這篇stackoverflow的回答也是這么說的:https://stackoverflow.com/questions/2234982/why-both-import-logging-and-import-logging-config-are-needed

后記

導入更深入的解釋可以看此文,有視頻可以幫助理解:Modules and Packages: Live and Let Die!
http://www.dabeaz.com/modulepackage/index.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,394評論 0 10
  • 今天我要分享的書是“你的燈亮著嗎?”,副標題是“發現問題的真正所在”。本書第一版發表于1982年,全書12.2萬字...
    肖穎閱讀 311評論 0 0
  • “對欲望不理解,人就永遠不能從桎梏和恐懼中解脫出來。如果你摧毀了你的欲望,可能你也摧毀了你的生活。如果你扭曲它,壓...
    莊主與五少爺閱讀 240評論 0 1
  • 天空飄著一朵奇怪的云 我有一個奇怪的笑容 像一朵白云 驀然而至 聚成白霧 化成煙雨 輕輕滴落在葉上 溜入滔滔的江湖...
    小妮子vi閱讀 628評論 1 2