前言
打算写一系列文章来记录自己学习 Python 3 的点滴;
本章主要介绍 Python 中的模块内容;
本文为作者的原创作品,转载需注明出处;
模块的定义
模块是什么
模块在 Python 中的定义非常的简单和直接,一个.py
的文件就是一个模块;模块,故名思议,它是构建一个系统的一个组成部分,同时可以被系统的其它部分所引用;比如我们定义了一个abc.py
,那么就对应一个abc
的模块;
包
为了避免同名模块之间的冲突,Python 中也引入了包的概念;如图,我们有两个同名的模块abc
;
不过这两个模块通过 package 的方式区分,一个模块是abc
,另一个模块是mycomponent.abc
;不过,这里要注意的是__init__.py
的文件,该文件实际上可以是一个空文件,它的作用却非常的重要,它告诉 Python,这是一个Package
,否则,将会当做一个普通的文件夹处理;
模块的内部结构
下面,笔者以一个非常简单的模块hello
来描述一下模块的内部结构,
hello.py1
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()
- 头两行是标准注解,第一行表示的是使用 python3 的环境,第二行表示源码的字符编码;
- 第四行是文档的注释,也可以使用
''' '''
来描述多行注解; - 第六行是作者信息
- 第八行,导入系统默认模块
sys
- 第十行,自定义模块内部的方法和类,类将在后续部分予以介绍;所以,这里我们应该清楚认识到的一点是,
方法
和类
都是模块
的组成部分
; - 第十一行,
sys
模块中的argv
参数存储了命令行中的所有参数,且第一个参数永远是.py
文件名; - 第十九行,
if __name__=='__main__'
,这行有意思了,这等价于 java 中的 main 函数,可以直接调用此模块;一般,用这样的方式来进行模块的测试;另外,当该模块hello
被其它模块所导入的时候,与该行相关的代码将会被自动忽略掉;
模块的安装和使用
该小节,笔者将介绍模块
在 Python 中是如何使用的;
模块的搜索路径
那么,我们的自定义模块或者第三方模块是如何被 Python 所加载的呢?它是按照如下的顺序进行检索的,
1 | import sys |
模块sys
通过其属性sys.path
定义了模块的加载路径和相关顺序,可见,Python 首先从当前路径下查找相应的模块,若没有找到,则到 Python 的内置模块中去查找,然后到安装第三方模块的 site_packages 中去查找;
如果,我们需要自定义检索的路径,有两种方式可以进行,
直接修改
sys.path
1
2import sys
'/Users/shangyang/mycomponents') sys.path.append(设置环境变量
PYTHONPATH
个人比较推荐使用 #2 的方式,因为这样的话,不会影响原有系统模块sys
中的内置参数;
使用自定义模块
在
~/tmp
目录下定义模块hello.py
1
2
3
4$ cd ~/tmp
$ touch hello.py
$ vim hello.py
... 输入《模块的内部结构》中的测试用例测试模块
hello.py
,这里会直接调用 Python 中与__main__
相关的函数,作为入口函数启动模块;1
2
3
4$ python3 hello.py
Hello, World!
$ python3 hello.py Michael
Hello, Michael!进入 Python Shell,导入自定义模块,
1
2
3$ python3
...
import hello这样,我们就导入了
hello
模块,从模块的搜索路径小节我们可以知道,Python 将会从当前路径中查找模块hello.py
,正好,hello.py
在当前路径中,所以,它可以被成功导入;1
2hello.test()
Hello, world!
使用第三方模块
安装第三方模块
比如我们要安装chardet
第三方模块,只需要使用如下语句即可,
1 | $ pip3 install chardet |
这里要注意的是,凡是第三方模块,都将会被安装到目录./3.6/lib/python3.6/site-packages
中;
如果需要卸载,执行如下命令,
1 | $ pip3 uninstall chardet |
使用第三方模块
这里,就是用上述chardet
模块为例,看看第三方模块是如何使用的,
使用第三方模块有两种方式,
使用
import <module name>
1
2
3
4'/tmp/tmp.jp', rb).read() rawdata = open(
import chardet
chardet.detect(rawdata)
{'encoding': 'EUC-JP', 'confidence': 0.99}使用
from <module name> import <object>
1
2
3
4'/tmp/tmp.jp', rb).read() rawdata = open(
from chardet import detect
detect(rawdata)
{'encoding': 'EUC-JP', 'confidence': 0.99}这种方式,可以从模块中只导入其中一个对象,注意,Python 中一个变量,方法,Class 都是对象;
main 方法
test.py
1 | #!/usr/bin/env python3 |
如何传递参数
http://blog.csdn.net/weixin_35653315/article/details/72886718
打包和发布模块
本文通过 pypi 官方的构建说明文档进行实践和整理 https://packaging.python.org/tutorials/distributing-packages/
通过 pypi
本章节将介绍如何通过 pypi 构建可以通过 pip install 下载并安装的包;
项目配置
仍然假设在本地文件夹 ~/tmp/test 中有单个文件的模块 helloworld.py
1
2#!/usr/bin/env python
print("Hello World")编写 ~/tmp/test/setup.py,注意,该文件是指导 pip 如何进行安装的描述文件,非常重要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from 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
编写 ~/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编写 ~/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 的构建过程中才会将这些文件进行打包;
- 所以,一开始,我们有如下的工程文件
1
2$ ls
LICENSE.txt MANIFEST.in helloworld.py setup.cfg setup.py
工程打包
首先创建 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 模块进行打包;
构建 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
在 home 路径中创建一个 pypi 的用户配置文件 ~/.pypirc,
1
2
3[pypi]
username = <username>
password = <password>安装 twine
1
$ pip install twine
发布
1
$ twine upload dist/*
twine 会负责将本地打包好的模块直接发布到 pypi 上;
- 这样,在 pypi 网站的个人信息的 Your projects 中就会显示相关的项目信息了,
点击 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 不期望看到的;所以,如果要重新发布
- 在 setup.py 中修改 version 的值;
- 修改代码;
- 使用 twine 提交,会生成新的历史版本;
备注,在笔者实际的测试过程当中,提交的过程中仍然会出现上面的错误,但是提交会成功,可以看到,它既是 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 章节中的项目配置小节中的内容;
使用
如果已经通过 pip install 的方式安装过了该模块,使用 pip uninstall 卸载,
1
$ pip uninstall helloworld-mytest
按照上一小节“工程打包”中的内容将项目打包,
然后使用如下的命令,使得当前的模块以开发模式导入到 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
表示的就是开发模式;
测试
进入 python 命令行,导入 helloworld 模块
1
2>>> import helloworld
Hello World直接修改该 helloworld.py 中的内容
1
2#!/usr/bin/env python
print("Hello World2")再次进入 python 命令行,导入 helloworld 模块
1
2>>> import helloworld
Hello World2可见,我们并没有重新通过 pip install 安装 helloworld-mytest 便可以直接使用其 helloworld.py 模块中所变化的内容;这将使得我们在开发过程中变得异常的高效;
同时导入多个模块
上面我们演示了如何以开发模式导入某一个模块,如果我们有多个模块需要导入呢?
可以使用路径的方式,比如
1
2pip install -e /path/to/project/bar
pip install -e .假设我们当前目录中的模块叫做 foo,在 /path/to/project/bar/ 中的是 bar 工程,那么上面的命令将会以开发模式同时导入 foo 和 bar;但是,如果我们的 bar 不再本地呢?而是在某个 Git 的代码仓库中呢?可以使用如下的方式,
可以使用 http 的方式,比如
1
2pip install -e .
pip install -e git+https://somerepo/bar.git#egg=bar如果不需要第三方包的任何依赖,可以使用如下的方式进行过滤,
1
pip install -e . --no-deps
更多详细内容参考 https://packaging.python.org/tutorials/distributing-packages/#working-in-development-mode 中的描述;
直接本地安装
如果不想通过 pypi 安装到本地,可以使用如下的方式,
1 | $ git clone |
直接通过本地的 setup.py 的描述文件安装到本地的 site-packages/ 目录中即可;
References
如何将自己的模块发布成可以通过 pip install 的包,