當前目錄 和 腳本目錄
參考資料: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的導入實際上是有很多學問的,弄懂它對開發運維都有巨大幫助, 我從上面這位斯坦福大學大二學生的教程中受益匪淺,決定把它編譯下來,順帶改正幾處他的筆誤。讀懂了上文,只要你不準備寫第三方庫,那么基本能夠應付幾乎所有的導入問題了。
關鍵點:
-
import
語句根據sys.path
的路徑進行搜索 -
sys.path
自動包含入口文件所在目錄,而不是包含working directory
-
import
一個package概念上就是導入該package的__init__.py
文件
基本概念
- module:
*.py
文件,module的名字為該文件的名字 - built-in module: 被編譯進python解釋器的模塊(用C實現),因此沒有對應的
*.py
文件 - package: 包含
__init__.py
的目錄,package的名字是目錄的名字- python 3.3以后,任何目錄都是package(即使沒有
__init__.py
)
- python 3.3以后,任何目錄都是package(即使沒有
例子的目錄結構
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.py
,sys.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 module
與built-in function
有區別, built-in function
可以在builtins
module找到,而builtins
本身又是built-in module.)
如上面的目錄結構中:time
是built-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個功能:
- 將目錄轉化為可導入的package(python3.3及其之前)
- 執行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 2, python3中不再支持:
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
中導入了sa2
,start.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種方案:
- 使用
from packA.subA import sa2
(中間一列),此時start.py
當然沒問題; 將命令行切換到test/
目錄下,運行python -m packA.a2
,就等于直接運行a2
了。 - 使用
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)。
- 使用第三列的寫法,可是只對python2有效
- 使用
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.path
或PYTHONPATH
來做到,就是用方案2, 4。
但是我建議:寫代碼時,盡量不要導入父級目錄的腳本。
其實pycharm的導入方式,就是修改了PYTHONPATH
和sys.path
,不過它設置的地方比較多,細節待整理:
python2與python3的區別
- python2支持
implicit relative import
, python3不支持 - python2的package要包含
__init__.py
,python3.3和更高版本把所有目錄都認為是package(implicit namespace packages) -
from <module> import *
語法在python2中可以寫在函數中,python3只允許寫在module一級。
其他散落的知識點
-
__init__.py
中使用__all__
-
pip install -e <project>
將project的root目錄加入sys.path
-
from <module> import *
不會將'_'開頭的名稱導入
- 使用
if __name__ == '__main__'
檢測腳本是直接運行還是被導入的
總結
對于一般的開發工作,上文介紹python的導入機制已經足夠用了。
但是如果你想開發package并且發布出去,那么再深入研究一番也是必須的。事實上,本文有大量知識點都有待深入挖掘,此外我覺得還可以研究一下pkgutil
和importlib
標準庫模塊。
補充:
寫此文已過去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
文件如下:
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