python import 詳解

當(dāng)前目錄 和 腳本目錄

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

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

導(dǎo)入詳解

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

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

關(guān)鍵點(diǎn):

  1. import 語句根據(jù)sys.path的路徑進(jìn)行搜索
  2. sys.path自動(dòng)包含入口文件所在目錄,而不是包含working directory
  3. import一個(gè)package概念上就是導(dǎo)入該package的__init__.py文件

基本概念

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

例子的目錄結(jié)構(gòu)

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做了什么?

當(dāng)import module時(shí),python會(huì)運(yùn)行其中的代碼;當(dāng)import package時(shí),python會(huì)運(yùn)行__init__.py中的代碼

import的搜索路徑

當(dāng)import spam時(shí),先搜索built-in module中是否有spam模塊,如果沒有則按順序搜索 sys.path下的是由有spam模塊。

假設(shè)運(yùn)行>> python script.pysys.path的路徑列表是這樣初始化的的:

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

初始化完成后,python程序可以更改sys.path. 此外因?yàn)槿肟谖募谀夸浥旁跇?biāo)準(zhǔn)庫之前,因此與標(biāo)準(zhǔn)庫同名的在該目錄下的自定義文件會(huì)替代標(biāo)準(zhǔn)庫被優(yōu)先導(dǎo)入。 Source: Python 2 and 3.

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

如上面的目錄結(jié)構(gòu)中:timebuilt-in module, 而random是 standard module, 因此在start.py文件中import time會(huì)導(dǎo)入的python的內(nèi)置module,但是 import random會(huì)導(dǎo)入我們自定義的module。

sys.path注意點(diǎn)

最后強(qiáng)調(diào)一遍:當(dāng)運(yùn)行一個(gè)python腳本時(shí),sys.path不關(guān)心工作目錄(當(dāng)前目錄)在哪里,只關(guān)心入口文件所在的目錄在哪里。例如:如果當(dāng)前目錄在test/下,執(zhí)行python ./packA/subA/subA1.py, sys.path[0]test/packA/subA/而不是test/。

此外sys.path是導(dǎo)入模塊所共享的。例如假設(shè)我們執(zhí)行python start.py, 而start.py中執(zhí)行了import packA.a1, 在a1中打印sys.path, 則它包含test/目錄(入口文件start.py的路徑), 而不是test/packA/(a1,py的路徑); 因此在a1.py中導(dǎo)入other應(yīng)該執(zhí)行import other ,因?yàn)?code>other.py在搜索路徑test/中。

__init__.py詳解

__init__.py有以下2個(gè)功能:

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

將目錄轉(zhuǎn)化為可導(dǎo)入的package

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

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

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

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

運(yùn)行package的初始化代碼

每當(dāng)import package時(shí),python會(huì)先執(zhí)行__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()

導(dǎo)入package

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

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

absolute導(dǎo)入與relative導(dǎo)入:重點(diǎn)

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

  • explicit relative imports: 以from .<module/package> import X形式的導(dǎo)入,前面的點(diǎn)表示當(dāng)前目錄,兩個(gè)點(diǎn)表示上一層目錄。。。
  • implicit relative import: 就好像當(dāng)前目錄在sys.path中的導(dǎo)入,它只適用于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

例子如下:
假設(shè)運(yùn)行入口文件是start.py, 它導(dǎo)入了a1, 而a1又導(dǎo)入了other, a1和sa1, 則a1中的導(dǎo)入方式可以這樣寫:

  • 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是不支持的,會(huì)報(bào):ValueError: attempted relative import beyond top-level package。 因此只能用absolute import。

建議只使用absolute import , 不僅是因?yàn)槊魑锥€因?yàn)橄鄬?dǎo)入的文件都無法直接運(yùn)行,而絕對導(dǎo)入的module可以通過某種方式運(yùn)行:將原來的入口文件所在目錄添加到sys.path中,下文將做詳細(xì)分析。

案例

Case1 不修改sys.path:

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

Case2 可以修改sys.path:

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

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無論哪種導(dǎo)入sa2的方式,要么運(yùn)行start.py時(shí)報(bào)錯(cuò),要么運(yùn)行a2時(shí)報(bào)錯(cuò),沒有同時(shí)都可以運(yùn)行成功的方案。
下面提供了3種方案:

  1. 使用from packA.subA import sa2(中間一列),此時(shí)start.py當(dāng)然沒問題; 將命令行切換到test/目錄下,運(yùn)行python -m packA.a2,就等于直接運(yùn)行a2了。
  2. 使用from packA.subA import sa2(中間一列),此時(shí)start.py當(dāng)然沒問題;我們可以在運(yùn)行a2前更改sys.path,將test加入搜索路徑, 這樣直接運(yùn)行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__有時(shí)候不準(zhǔn)確,可以使用 built-in inspect包來解決(this StackOverflow answer)。

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

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

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

Case3

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

Case4:導(dǎo)入父級目錄的module

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

其實(shí)pycharm的導(dǎo)入方式,就是修改了PYTHONPATHsys.path,不過它設(shè)置的地方比較多,細(xì)節(jié)待整理:

image1.png

image2.png

image3.png

python2與python3的區(qū)別

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

其他散落的知識點(diǎn)

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

總結(jié)

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

補(bǔ)充:

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

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

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

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

其實(shí)看一下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是一個(gè)單獨(dú)的模塊,即使導(dǎo)入了logging,該模塊也未導(dǎo)入,因此不能使用;而getLogger定義在__init__.py中,導(dǎo)入logging.config的時(shí)候也導(dǎo)入了logging,因此__init__.py中的任何函數(shù)都可以直接使用。
如果只是使用getLogger,只導(dǎo)入logging而不導(dǎo)入logging.config也是可以的:

import logging

logger = logging.getLogger(__name__)

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

后記

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容