一些操作和知识点代码之美technology accumulation

python import 详解

2018-08-20  本文已影响238人  ThomasYoungK

当前目录 和 脚本目录

参考资料: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文件

基本概念

例子的目录结构

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种:

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中的导入方式可以这样写:

import other
import packA.a2
import packA.subA.sa1
import other  # absolute imports
from . import a2 # explict relative imports
from .subA import sa1  # explict relative imports
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__
  1. pip install -e <project>将project的root目录加入sys.path
  1. from <module> import *不会将'_'开头的名称导入
  1. 使用if __name__ == '__main__'检测脚本是直接运行还是被导入的

总结

对于一般的开发工作,上文介绍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

上一篇下一篇

猜你喜欢

热点阅读