Python 系列学习之七:模块

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;

本章主要介绍 Python 中的模块内容;

本文为作者的原创作品,转载需注明出处;

模块的定义

模块是什么

模块在 Python 中的定义非常的简单和直接,一个.py的文件就是一个模块;模块,故名思议,它是构建一个系统的一个组成部分,同时可以被系统的其它部分所引用;比如我们定义了一个abc.py,那么就对应一个abc的模块;

为了避免同名模块之间的冲突,Python 中也引入了包的概念;如图,我们有两个同名的模块abc

不过这两个模块通过 package 的方式区分,一个模块是abc,另一个模块是mycomponent.abc;不过,这里要注意的是__init__.py的文件,该文件实际上可以是一个空文件,它的作用却非常的重要,它告诉 Python,这是一个Package,否则,将会当做一个普通的文件夹处理;

模块的内部结构

下面,笔者以一个非常简单的模块hello来描述一下模块的内部结构,

hello.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

import sys

def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')

if __name__=='__main__':
test()

  1. 头两行是标准注解,第一行表示的是使用 python3 的环境,第二行表示源码的字符编码;
  2. 第四行是文档的注释,也可以使用''' '''来描述多行注解;
  3. 第六行是作者信息
  4. 第八行,导入系统默认模块 sys
  5. 第十行,自定义模块内部的方法和类,类将在后续部分予以介绍;所以,这里我们应该清楚认识到的一点是,方法都是模块组成部分
  6. 第十一行,sys模块中的argv参数存储了命令行中的所有参数,且第一个参数永远是.py文件名;
  7. 第十九行,if __name__=='__main__',这行有意思了,这等价于 java 中的 main 函数,可以直接调用此模块;一般,用这样的方式来进行模块的测试;另外,当该模块hello被其它模块所导入的时候,与该行相关的代码将会被自动忽略掉;

模块的安装和使用

该小节,笔者将介绍模块在 Python 中是如何使用的;

模块的搜索路径

那么,我们的自定义模块或者第三方模块是如何被 Python 所加载的呢?它是按照如下的顺序进行检索的,

1
2
3
>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']

模块sys通过其属性sys.path定义了模块的加载路径和相关顺序,可见,Python 首先从当前路径下查找相应的模块,若没有找到,则到 Python 的内置模块中去查找,然后到安装第三方模块的 site_packages 中去查找;

如果,我们需要自定义检索的路径,有两种方式可以进行,

  1. 直接修改sys.path

    1
    2
    >>> import sys
    >>> sys.path.append('/Users/shangyang/mycomponents')
  2. 设置环境变量PYTHONPATH

个人比较推荐使用 #2 的方式,因为这样的话,不会影响原有系统模块sys中的内置参数;

使用自定义模块

  1. ~/tmp目录下定义模块hello.py

    1
    2
    3
    4
    $ cd ~/tmp
    $ touch hello.py
    $ vim hello.py
    ... 输入《模块的内部结构》中的测试用例
  2. 测试模块hello.py,这里会直接调用 Python 中与 __main__ 相关的函数,作为入口函数启动模块;

    1
    2
    3
    4
    $ python3 hello.py
    Hello, World!
    $ python3 hello.py Michael
    Hello, Michael!
  3. 进入 Python Shell,导入自定义模块,

    1
    2
    3
    $ python3
    ...
    >>> import hello

    这样,我们就导入了hello模块,从模块的搜索路径小节我们可以知道,Python 将会从当前路径中查找模块hello.py,正好,hello.py在当前路径中,所以,它可以被成功导入;

    1
    2
    >>> hello.test()
    Hello, world!

使用第三方模块

安装第三方模块

比如我们要安装chardet第三方模块,只需要使用如下语句即可,

1
2
$ pip3 install chardet
.... installed into ./3.6/lib/python3.6/site-packages

这里要注意的是,凡是第三方模块,都将会被安装到目录./3.6/lib/python3.6/site-packages中;

如果需要卸载,执行如下命令,

1
$ pip3 uninstall chardet

使用第三方模块

这里,就是用上述chardet模块为例,看看第三方模块是如何使用的,

使用第三方模块有两种方式,

  1. 使用import <module name>

    1
    2
    3
    4
    >>> rawdata = open('/tmp/tmp.jp', rb).read()
    >>> import chardet
    >>> chardet.detect(rawdata)
    {'encoding': 'EUC-JP', 'confidence': 0.99}
  2. 使用from <module name> import <object>

    1
    2
    3
    4
    >>> rawdata = open('/tmp/tmp.jp', rb).read()
    >>> from chardet import detect
    >>> detect(rawdata)
    {'encoding': 'EUC-JP', 'confidence': 0.99}

    这种方式,可以从模块中只导入其中一个对象,注意,Python 中一个变量,方法,Class 都是对象;

main 方法

test.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

import sys

def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')

if __name__=='__main__':
test()

如何传递参数

http://blog.csdn.net/weixin_35653315/article/details/72886718

打包和发布模块

本文通过 pypi 官方的构建说明文档进行实践和整理 https://packaging.python.org/tutorials/distributing-packages/

通过 pypi

本章节将介绍如何通过 pypi 构建可以通过 pip install 下载并安装的包;

项目配置

  1. 仍然假设在本地文件夹 ~/tmp/test 中有单个文件的模块 helloworld.py

    1
    2
    #!/usr/bin/env python
    print("Hello World")
  2. 编写 ~/tmp/test/setup.py,注意,该文件是指导 pip 如何进行安装的描述文件,非常重要

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from setuptools import setup, find_packages

    setup(
    name='helloworld-mytest', # This is the name of your PyPI-package.
    version='0.1.1', # Update the version number for new releases
    description='just a test module',
    # packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Required
    # Alternatively, if you just want to distribute a single Python file, use
    # the `py_modules` argument instead as follows, which will expect a file
    # called `my_module.py` to exist:
    #
    # py_modules=["my_module"],
    py_modules=["helloworld"]

    )

    如果是单个文件需要使用 py_modules,如果是包,那么使用 packages;通过 name 设置 pypi 模块的名称为 helloworld-mytest,该名字特别的重要,且在 pypi 中全局唯一(后面会再次讨论该话题),如果发布成功,则可以通过命令

    1
    pip install helloworld-mytest

    安装自己的模块了;version 字段非常关键,描述了当前模块的版本;有关 setup.py 的说明参考 https://github.com/pypa/sampleproject/blob/master/setup.py

  3. 编写 ~/tmp/test/setup.cfg

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [metadata]
    # This includes the license file in the wheel.
    license_file = LICENSE.txt

    [bdist_wheel]
    # This flag says to generate wheels that support both Python 2 and Python
    # 3. If your code will not run unchanged on both Python 2 and 3, you will
    # need to generate separate wheels for each Python version that you
    # support. Removing this line (or setting universal to 0) will prevent
    # bdist_wheel from trying to make a universal wheel. For more see:
    # https://packaging.python.org/tutorials/distributing-packages/#wheels
    universal=1

    里面包含了两个配置元素 metadata 和 bdist_wheel,metadata 表示相关的元数据,这里配置了一个相关的 license file;然后配置 bdist_wheel,注意 bdist_wheel 有两种种选择,1、是否兼容 python2 和 python3;2、是否只兼容 python2 或者 python3;这里配置的是 universal=1 表示兼容 python2 和 python3,如果设置为 0 表示只兼容其中一种;也可以通过下面的命令行的方式来表述 universal=1,

    1
    python setup.py bdist_wheel --universal

    universal=0

    1
    pip install wheel

    这样,在构建的时候它会根据当前 python 的环境生成相应的 python 版本的编译文件;更多标准描述参考
    参考 https://github.com/pypa/sampleproject/blob/master/setup.cfg

  4. 编写 ~/tmp/test/MANIFEST.in

    1
    2
    3
    4
    5
    6
    7
    8
    # Include the README
    include *.md

    # Include the license file
    include LICENSE.txt

    # Include the data files
    recursive-include data *

    默认情况下,python 的打包过程只会将 .py 的模块文件和其对应的包进行打包,并不会对其它的文件进行打包,所以,如果项目中引用了其它的配置文件,比如 *.md,*.txt,*.properties 文件等等,需要在这里显式的进行添加,这样在 python 的构建过程中才会将这些文件进行打包;

  5. 所以,一开始,我们有如下的工程文件
    1
    2
    $ ls
    LICENSE.txt MANIFEST.in helloworld.py setup.cfg setup.py

工程打包

  1. 首先创建 Source Distribution

    1
    python setup.py sdist

    该步骤完成以后,会在 ~/tmp/test/ 目录中生成 dist/ 目录,

    1
    2
    $ ls dist
    helloworld-mytest-0.1.1.tar.gz

    可见,会根据 setup.py 中的 version 信息对当前的 helloworld.py 模块进行打包;

  2. 构建 wheel,在前文中笔者已经进行过描述,wheel 主要是用来描述当前的 python 模块是否只支持 python2 或者 python3,还是两者都支持 universal? 要能使用 wheel 进行构建,我们得首先对其进行安装,使用如下命令,

    1
    pip install wheel

    前面我们已经通过配置文件 setup.cfg 指定了是 universal 既支持 python2 和 python3,所以,我们使用如下的命令进行构建,

    1
    $ python setup.py bdist_wheel --universal

    该命令将会在 ~/tmp/test/dist/ 目录中生成相应的配置信息,

    1
    2
    $ ls
    helloworld-mytest-0.1.1.tar.gz helloworld_mytest-0.1.1-py2.py3-none-any.whl

    从生成的文件名中可以看到,该模块是同时支持 py2 和 py3 的;

更多有关 wheel 的介绍,参考 https://packaging.python.org/tutorials/distributing-packages/#wheels

注册 pypi

要想将 python 的打包文件载入 pypi 并且能够直接通过 pip 命令安装自己的模块,那么首先需要注册一个 pypi 的账号,注册地址,注册的过程非常的简单,提供自己的邮箱地址和用户名即可,只要要特别注意的是,注册以后,需要通过邮箱验证才能激活,否则不能使用;注册成功以后,就可以通过个人中心查看到自己所发布的 projects 了,当然,现在我们没有任何的 projects;

发布到 pypi

  1. 在 home 路径中创建一个 pypi 的用户配置文件 ~/.pypirc,

    1
    2
    3
    [pypi]
    username = <username>
    password = <password>
  2. 安装 twine

    1
    $ pip install twine
  3. 发布

    1
    $ twine upload dist/*

    twine 会负责将本地打包好的模块直接发布到 pypi 上;

  4. 这样,在 pypi 网站的个人信息的 Your projects 中就会显示相关的项目信息了,
    pypi my project.png
    点击 view,然后在 Release history 中会显示出发布的历史信息;

安装 helloworld-mytest 模块

在本地,通过

1
$ pip install helloworld-mytest

既可以安装 helloworld-mytest 包到 python 的 site-packages/ 目录中了,这样所有的 python 工程都可以使用该模块了;

重新发布

这里要特别注意的是,一旦你的包以某个版本号发布过了,但是发现该版本有些问题,想基于该版本继续发布修改该后的内容,这种行为在 pypi 中是不允许的,否则将会抛出如下的错误,HTTPError: 400 Client Error: This filename has previously been used, you should use a different version. See https://pypi.org/help/#file-name-reuse for url: https://upload.pypi.org/legacy/ 所以,在每次重新发布的时候,必须确保是新的 version,不能对现有的 version 进行更改,这就要求开发者在发布之前对自己的代码进行严格的审核和检查,pypi 官方给出的解释是,如果允许对现有版本进行修改和调整,会影响到在修改之前就已经 install 该版本模块的用户的使用,会导致同一个版本不同的代码,这是 pypi 不期望看到的;所以,如果要重新发布

  1. 在 setup.py 中修改 version 的值;
  2. 修改代码;
  3. 使用 twine 提交,会生成新的历史版本;
    pypi my project - release history.png
    备注,在笔者实际的测试过程当中,提交的过程中仍然会出现上面的错误,但是提交会成功,可以看到,它既是 error 实际上同时也是 warning;

检测包名是否可用
使用 testpypi 来检测包名是否可用,可以直接通过下面的命令执行,

1
$ twine upload --repository-url https://test.pypi.org/legacy/ dist/*

也可以通过配置的方式在发布的时候自动检测,详情参考 https://packaging.python.org/guides/using-testpypi/

development mode

该模式的作用是表示将本地的某个工程目录作为开发模式的包进行引用;该特性在开发过程中尤为的重要,它会使得 python 动态的引用该模块中的最新的代码;假设在本地文件夹 ~/tmp/test 中有单个文件的模块 helloworld.py 模块,相关的项目文件和配置文件参考通过 pypi 章节中的项目配置小节中的内容;

使用

  1. 如果已经通过 pip install 的方式安装过了该模块,使用 pip uninstall 卸载,

    1
    $ pip uninstall helloworld-mytest
  2. 按照上一小节“工程打包”中的内容将项目打包,

  3. 然后使用如下的命令,使得当前的模块以开发模式导入到 python 库中

    1
    2
    3
    4
    5
    $ pip install -e .
    Obtaining file:///Users/mac/tmp/test
    Installing collected packages: helloworld-mytest
    Running setup.py develop for helloworld-mytest
    Successfully installed helloworld-mytest

    要特别特别注意的是,以这种方式导入的模块 python 并不会将其放置到 site-packages/ 目录中,而只是在 python 中建立了一个软链接引用的就是 ~/tmp/test/ 中所定义的 helloworld-mytest(通过 setup.py 中定义的),正是这种机制使得 helloworld-mytest 模块中的任意的改动,都会立即反馈到引用了它的应用当中;该软链接如下,

    1
    2
    $ ls ..../site-packages/
    .... helloworld-mytest.egg-link ....

    正是通过 helloworld-mytest.egg-link 连接到的 ~/temp/test/ 的 helloworld-mytest 模块的;如果要卸载,使用如下命令,

    1
    2
    3
    4
    5
    6
    $ pip3 uninstall helloworld-mytest
    Uninstalling helloworld-mytest-0.1.3:
    Would remove:
    /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/helloworld-mytest.egg-link
    Proceed (y/n)? y
    Successfully uninstalled helloworld-mytest-0.1.3

    可见只需要将该软链接 helloworld-mytest.egg-link 删除即可;

要特别注意的是,命令-e表示的就是开发模式;

测试

  1. 进入 python 命令行,导入 helloworld 模块

    1
    2
    >>> import helloworld
    Hello World
  2. 直接修改该 helloworld.py 中的内容

    1
    2
    #!/usr/bin/env python
    print("Hello World2")
  3. 再次进入 python 命令行,导入 helloworld 模块

    1
    2
    >>> import helloworld
    Hello World2

    可见,我们并没有重新通过 pip install 安装 helloworld-mytest 便可以直接使用其 helloworld.py 模块中所变化的内容;这将使得我们在开发过程中变得异常的高效;

同时导入多个模块

上面我们演示了如何以开发模式导入某一个模块,如果我们有多个模块需要导入呢?

  1. 可以使用路径的方式,比如

    1
    2
    pip install -e /path/to/project/bar
    pip install -e .

    假设我们当前目录中的模块叫做 foo,在 /path/to/project/bar/ 中的是 bar 工程,那么上面的命令将会以开发模式同时导入 foo 和 bar;但是,如果我们的 bar 不再本地呢?而是在某个 Git 的代码仓库中呢?可以使用如下的方式,

  2. 可以使用 http 的方式,比如

    1
    2
    pip install -e .
    pip install -e git+https://somerepo/bar.git#egg=bar
  3. 如果不需要第三方包的任何依赖,可以使用如下的方式进行过滤,

    1
    pip install -e . --no-deps

更多详细内容参考 https://packaging.python.org/tutorials/distributing-packages/#working-in-development-mode 中的描述;

直接本地安装

如果不想通过 pypi 安装到本地,可以使用如下的方式,

1
2
3
$ git clone
$ cd silex
$ python setup.py install

直接通过本地的 setup.py 的描述文件安装到本地的 site-packages/ 目录中即可;

References

如何将自己的模块发布成可以通过 pip install 的包,

  1. http://marthall.github.io/blog/how-to-package-a-python-app/
  2. 官方教程 https://packaging.python.org/tutorials/distributing-packages/
  3. pypi: https://pypi.org/