转载 刘畅的博客 . 文章地址 .本文所涉及到的代码在 github 上
概述python 是一门优美简单、功能强大的动态语言。在刚刚接触这门语言时,我们会被其优美的格式、简洁的语法和无穷无尽的类库所震撼。在真正的将python应用到实际的项目中,你会遇到一些无法避免的问题。最让人困惑不解的问题有二类,一个 编码问题,另一个则是引用问题。
本文主要讨论关于Python中import的机制与实现、以及介绍一些有意思的Python Hooks。
Python 类库引入机制首先,看一个简单的例子:
""" 目录结构如下: ├── __init__.py ├── main.py └── string.py """ # main.py 内容如下 import string print string.a # string.py 内容如下 a = 2现在,考虑一下:
当我们执行main.py的时候,会发生什么事情? 在main.py文件执行到 import string 的时候,解释器导入的string类库是当前文件夹下的string.py还是系统标准库的string.py呢? 如果明确的指明己要引的类库?为了搞清楚上面的问题,我们需要了解关于Python类库引入的机制。
Python的两种引入机制Python 提供了二种引入机制:
relative import absolute import relative importrelative import 也叫作相对引入,在Python2.5及之前是默认的引入方法。它的使用方法如下:
from .string import a from ..string import a from ...string import a这种引入方式使用一个点号来标识引入类库的精确位置。与linux的相对路径表示相似,一个点表示当前目录,每多一个点号则代表向上一层目录。
""" ├── __init__.py ├── foo.py └── main.py """ # foo.py a = 2 # main.py print __name__ from .foo import a print a相对引入,那么我们需要知道相对什么来引入。相对引入使用被引入文件的 __name__ 属性来决定该文件在整个包结构的位置。那么如果文件的 __name__ 没有包含任何包的信息,例如 __name__ 被设置为了 __main__ ,则认为其为‘top level script',而不管该文件的位置,这个时候相对引入就没有引入的参考物。如上面的程序所示,当我们执行 python main.py 时,Python解释器会抛出 ValueError: Attempted relative import in non-package的异常。
为了解决这个问题, PEP 0366 -- Main module explicit relative imports 提出了一个解决方案。允许用户使用 python -m ex2.main 的方式,来执行该文件。在这个方案下,引入了一个新的属性 __package__ 。
%m─liuchang@localhost ~/Codes/pycon %p─$ cat ex2/main.py print __name__ print __package__ from .foo import a print a %m─liuchang@localhost ~/Codes/pycon %p─$ python -m ex2.main __main__ ex2 2 absolute importabsolute import 也叫作完全引入,非常类似于Java的引入进制,在Python2.5被完全实现,但是是需要通过 from __future__ import absolute_import 来打开该引入进制。在Python2.6之后以及Python3,完全引用成为Python的默认的引入机制。它的使用方法如下:
from pkg import foo from pkg.moduleA import foo要注意的是,需要从包目录最顶层目录依次写下,而不能从中间开始。
在使用该引入方式时,我们碰到比较多的问题就是因为位置原因,Python找不到相应的库文件,抛出ImportError的异常。让我们看一个完全引用的例子:
""" ex3 ├── __init__.py ├── foo.py └── main.py """ # foo.py a = 2 # main.py print __name__ print __package__ from ex2.foo import a print a我们尝试着去运行main.py文件,Python解释器会抛出ImportError。那么我们如何解决这个问题呢?
%p─$ python ex3/main.py __main__ None Traceback (most recent call last): File "ex3/main.py", line 3, in <module> from ex2.foo import a ImportError: No module named ex2.foo首先,我们也可以使用前文所述的module的方式去运行程序,通过-m参数来告诉解释器 __package__ 属性。如下:
%m─liuchang@liuchangdeMacBook-Pro ~/Codes/pycon %p─$ python -m ex3.main __main__ ex3 2另外,我们还有一个办法可以解决该问题,在描述之前,我们介绍一个关于Python的非常有用的小知识: Python解释器会自动将当前工作目录添加到sys.path 。如下所示,可以看到我们打印出的 sys.path 已经包含了当前工作目录。
%m─liuchang@liuchangdeMacBook-Pro ~/Codes/pycon/ex4 %p─$ cat main.py import sys print sys.path %m─liuchang@liuchangdeMacBook-Pro ~/Codes/pycon/ex4 %p─$ python main.py ['/Users/liuchang/Codes/pycon/ex4', '/Library/Python/2.7/site-packages/pip-7.1.0-py2.7.egg', '/Library/Python/2.7/site-packages/mesos-_PACKAGE_VERSION_-py2.7.egg', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python27.zip', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-darwin', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac/lib-scriptpackages', '/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-old', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload', '/Users/liuchang/Library/Python/2.7/lib/python/site-packages', '/usr/local/lib/python2.7/site-packages', '/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC', '/Library/Python/2.7/site-packages']了解了Python解释器的这个特性后,我们就可以解决完全引用的找不到类库的问题:执行的时候,让解释器自动的将类库的目录添加到PYTHONPATH中。
我们可以在顶层目录中添加一个run_ex3.py的文件,文件内容和运行结果如下,可以看到Python解释器正确的执行了ex3.main文件。
%m─liuchang@liuchangdeMacBook-Pro ~/Codes/pycon %p─$ cat run_ex3.py from ex3 import main %m─liuchang@liuchangdeMacBook-Pro ~/Codes/pycon %p─$ python run_ex3.py ex3.main None 2 一些实践经验 相对引用还是绝对引用?上面介绍了Python的两种引用方式,都可以解决引入歧义的问题。那我们应该使用哪一种呢?
先说明一下Python的默认引用方式,在Python2.4及之前,Python只有相对引用这一种方式,在Python2.5中实现了绝对引用,但默认没有打开,需要用户自己指定使用该引用方式。在之后的版本和Python3版本,绝对引用已经成为默认的引用方式。
其次,二种引用方式各有利弊。绝对引用代码更加清晰明了,可以清楚的看到引入的包名和层次,但是,当包名修改的时候,我们需要手动修改所有的引用代码。相对引用则比较精简,不会被包名修改所影响,但是可读性较差,不如完全引用清晰。
最后,对于两种引用的方式选择,还是有争论的。在PEP8中,Python官方推荐的是绝对引用,详细理由可以参考 这儿 。
Absolute imports are recommended, as they are usually more readable and tend to be better behaved (or at least give better error messages) if the import system is incorrectly configured (such as when a directory inside a package ends up on sys.path ):
import mypkg.sibling from mypkg import sibling from mypkg.sibling import exampleHowever, explicit relative imports are an acceptable alternative to absolute imports, especially when dealing with complex package layouts where using absolute imports would be unnecessarily verbose:
from . import sibling from .sibling import exampleStandard library code should avoid complex package layouts and always use absolute imports.Implicit relative imports should never be used and have been removed in Python 3.
规范打包发布为了别人使用自己代码的方便,应该尽量使用规范的包分发机制。为自己的Python包编写正确的setup.py文件,添加相应的README.md文件。对于提供一些可执行命令的包,则可以使用 console_entrypoint 的机制来提供。因为打包和分发不是本文重点,不再详细叙述,大家可以查看官方文档。
使用virtualenv管理包依赖在使用Python的时候,尽量使用virtualenv来管理项目,所有的项目从编写到运行都在特定的virtualenv中。并且为自己的项目生成正确的依赖描述文件。
pip freeze > requirements.txt关于virtualenv的用法,可以参考我之前的一篇文章 virtualenv教程 。
Python import实现Python 提供了 import 语句来实现类库的引用,下面我们详细介绍当执行了 import 语句的时候,内部究竟做了些什么事情。
当我们执行一行 from package import module as mymodule 命令时,Python解释器会查找package这个包的module模块,并将该模块作为mymodule引入到当前的工作空间。所以import语句主要是做了二件事:
查找相应的module 加载module到local namespace下面我们详细了解python是如何查找模块的。
查找module的过程在import的第一个阶段,主要是完成了查找要引入模块的功能,这个查找的过程如下:
检查 sys.modules (保存了之前import的类库的缓存),如果mo