# 0.基本介绍

Python的强大之处就是它的丰富的开源包,如何开发一个自己的Python Package并将其发布到Pypi呢?在这里记录一下完整的过程。

# 1.__init__.py文件

python 中的包分成两种,一种是普通包regular package,一种是命名空间包namespace package

# 1.1 Regular Package

普通包是指包含一个 __init__.py文件的普通目录,在Python 3.2及之前的版本中一直使用的都是这种包。普通包在导入的时候,会隐式自动调用包中的__init__.py文件,__init__.py中定义的对象,变量都会被限制在包名的命名空间中。

如下形式的包:

parent/
    __init__.py
    one/
        __init__.py
    two/
        __init__.py
    three/
        __init__.py

导入parent.one包时,会自动执行parent/__init__.pyparent/one/__init__.py,导入其他的包时情况也类似。

总结,只需要记住,__init__.py是包导入时最先执行的文件

_init__.py中还有一个重要的变量,__all__, 它用来将模块全部导入,也就是支持使用from package import *

__all__ = ['os', 'sys', 're', 'urllib']

# 1.2 namespace package

命名空间包是指一种特殊的 Python 包,在Python 3.3之后引入,它不包含__init__.py文件,而是由一个或多个不同的目录组成,每个目录都可以包含一个或多个模块。

如有一个命名空间包namespace packagensp,在目录/xx/test_cpp/mypkg/ns/xx/test_cpp/mypkg/pkg下各有一个nsp是文件夹,其中分别包含nsp1.pynsp2.py文件,将路径/xx/test_cpp/mypkg/pkg/xx/test_cpp/mypkg/ns加入到sys.path中,就可以通过import nsp导入文件夹了。

# /xx/test_cpp/mypkg/nsa/nsp/nsp1.py
def print_nsp1():
     print(f"nsp1")

# /xx/test_cpp/mypkg/nsa/nsp/nsp2.py
def print_nsp2():
     print(f"nsp2")

sys.path.insert(-1, "/xx/test_cpp/mypkg/nsa")
sys.path.insert(-1, "/xx/test_cpp/mypkg/nsb")

import nsp # 这里会将两个路径下的nsp合成一个
nsp.print_nsp1()
nsp.print_nsp2()

注意两个文件夹nsp中都不能包含nsp.py,否则无法导入另外一个。

使用 namespace package命名包时,Python包的搜寻规则为,先搜寻常规的package,再搜寻module文件,然后若搜索到不包含__init__.py文件的目录即namespace package会先记录其路径,待sys.path路径搜寻完毕,若regular packagemodule中都没有而namespace package中有,就导入namespace pakge

# 2.Python Package工程

一个Python Package工程目录示例:

packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── src/
│   └── packaging_tutorial/
│       ├── __init__.py
│       └── example.py
└── tests/
  • tests文件夹中放的是包的单元测试文件
  • pyproject.toml文件告诉前端打包工具如pip/build等应该使用哪个后端工具如setuptools/ Hatchling等创建工程的发布包。
[build-system]
# requires是构建包需要的依赖,不需要手动的安装,前端工具如`pip`等会自动安装
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

除了指定编译系统之外,还需要为包添加元数据metadata,依赖dependencies和内容contents等,这些内容可以保存在pyproject.toml中,也可单独写到setup.py或者setup.cfg文件中。

如‵setup.py`


from setuptools import setup

setup(
    name='mypackage',
    version='0.0.1',
    install_requires=[
        'requests',
        'importlib-metadata; python_version == "3.8"',
    ],
)

或者setup.cfg:

[metadata]
name = mypackage
version = 0.0.1

[options]
install_requires =
    requests
    importlib-metadata; python_version < "3.8"s

setup方法中接受的参数以及含义介绍可以参考这里 (opens new window)

setup.py/setup.cfgpyproject.toml文件有重复配置的内容时,优先使用pyproject.toml中的内容。

# 2.1 安装及打包并发布到pypi

  • 开发模式安装本地包
pip install --editable .
  • 打包包文件,执行完成后,在包路径下生成dist文件夹,其中有打包好的文件
python -m build

# dist
# ├── mypkgg-0.0.1-py3-none-any.whl
# └── mypkgg-0.0.1.tar.gz
  • 将包上传到pypi
# 1.先安装twine包

pip install twine

python3 -m twine upload --repository testpypi dist/*

输入pypi的账号,密码后就可以将包上传到pypi数据库中了,网站上访问显示如下:

  • 如果git仓库是package,还可以通过如下形式安装:
pip install -q git+https://github.com/lx-r/mypkg.git

python中,为了避免setup.py的误执行,现在更推荐使用setup.cfg

上面包中的文件都是手动创建的,其实每个Python包都需要包含上述文件,如此就可以使用模板来创建Python包,常用的工具有cookiecutter (opens new window)等。

# 2.2 将Python文件编译成.so

需要使用cython

pip install cython

setup.py文件中设置

from Cython.Build import cythonize
setup(ext_modules = cythonize(["hello1.py", "hello2.py"]))

执行编译命令:

python setup.py build_ext

值得注意的是这里打包的.so文件只能适用于同样平台的同样python版本。

# 3.包的搜索路径

先搜寻built-in模块module,内建模块在变量sys.builtin_module_names中。

然后到sys.path路径中搜寻,sys.path中包含以下部分,

  • 当前路径
  • 环境变量PYTHONPATH
  • 缺省的安装路径如site-packages

关于包内的导入方式,当包中包含子包的时候,可以使用绝对路径导入,也可以通过相对路径的方式导入:

# mypkg
├── __init__.py
├── mypkg.py
└── utils
    ├── __init__.py
    └── utils.py

导入方式:

from .utils import utils
# in utils.py
from ..mypkg import xx

值得注意的是,相对路径的引入方式都是基于当前模块的名称__name__的,而在主模块中其__name__==__main__,因此在主模块函数中必须使用绝对导入方式。

(adsbygoogle = window.adsbygoogle || []).push({});

# 参考资料