创建高质量Python项目

  • 代码风格
  • 构建一个package
  • 项目结构

都是一些编程的经验之谈,大家可能会有比较多的意见或看法,欢迎一起探讨。

>>> import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Beautiful is better than ugly.

优美胜于丑陋(Python以编写优美的代码为目标)

Explicit is better than implicit.

明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)

Simple is better than complex.

简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)

Flat is better than nested.

复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)

Sparse is better than dense.

扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)

Readability counts.

可读性很重要(优美的代码是可读的)

Special cases aren't special enough to break the rules.

Although practicality beats purity.

即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)

Errors should never pass silently.

Unless explicitly silenced.

不要包容所有错误,除非您确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码)

In the face of ambiguity, refuse the temptation to guess.

当存在多种可能,不要尝试去猜测

There should be one-- and preferably only one --obvious way to do it.

而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)

Although that way may not be obvious at first unless you're Dutch.

虽然这并不容易,因为您不是 Python 之父(这里的 Dutch 是指 Guido )

Now is better than never.

Although never is often better than right now.

做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)

If the implementation is hard to explain, it's a bad idea.

If the implementation is easy to explain, it may be a good idea.

如果您无法向人描述您的方案,那肯定不是一个好方案;反之亦然(方案测评标准)

Namespaces are one honking great idea -- let's do more of those!

命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

代码风格

Python语法简单,没太多的约束。

但是,最基本的代码规范还是要遵循的,正所谓无规矩不成方圆。如果没有统一的规范,不一致的开发风格会给协作开发带来困难。

遵循PEP8规范,符合代码规范的代码, 会更受欢迎,更容易阅读

Google 开源项目风格指南 Python语言规范

Google 开源项目风格指南 Python风格规范

Pocoo Styleguide

常用的质量控制工具:

  • pycodestyle(pep8)
  • pylint
  • flake8
  • autopep8

构建一个package

包(package)是 Python 中对模块的更高一级的抽象。

封装成包是很简单的。在文件系统上组织你的代码,并确保每个目录都定义了一个__init__.py文件。

例如:

simple
├── core.py
├── exceptions.py
├── helpers.py
└── __init__.py

一旦你做到了这一点,你应该能够执行各种import语句,如下:

import sample.core
# import sample.core as core
from sample import core

最顶级包名是写给别人用的,然而在包内部也会有彼此之间互相导入的需求

在包内,既可以使用相对路径也可以使用绝对路径来导入。

.表示当前文件所在目录,..表示上一级目录。

# sample/core.py
from . import helpers

__init__.py能够用来自动加载子模块。

# sample/__init__.py
from .core import foo
from .helpers import bar
simple
├── core.py
├── exceptions.py
├── helpers.py
└── __init__.py
xxx.py

我们使用这个包的时候

# xxx.py
from sample import foo
from sample import bar

# 也可以这样导入
# from sample.core import foo
# from sample.helpers import bar

即使没有__init__.py文件存在,python仍然会导入包。如果你没有定义__init__.py时,实际上创建了一个所谓的命名空间包

读取包内的数据文件

首先,一个包对解释器的当前工作目录几乎没有控制权,因此,任何I/O操作都必须使用绝对路径,而不是相对路径。

每个模块包含有完整路径的__file__变量,可以根据这个变量来处理包内文件路径问题。

import os

# 获取当前文件所在目录
here = os.path.abspath(os.path.dirname(__file__))

# 获取上一级目录
pardir = os.path.abspath(os.path.join(here, os.path.pardir))

更高级的用法,使用pkgutil模块

异常处理

当一个函数变得复杂,在函数体中使用多返回值的语句并不少见。如返回一个值(比如说None、False或错误码)来表明 函数无法正确运行

然后在其它地方频繁使用if判断函数有无成功运行。

这样做是可以用来处理异常的,但存在代码可读性和维护性差的问题。

def complex_function(a, b, c):
    if not a:
        return None  # 抛出一个异常可能会更好
    if not b:
        return None  # 抛出一个异常可能会更好

    # 一些复杂的代码试着用a,b,c来计算x
    # 如果成功了,抵制住返回x的诱惑
    if not x:
        # 一些关于x的计算的Plan-B
    return x  # 返回值x只有一个出口点有利于维护代码

关于使用返回值还是使用异常的建议:

  1. 我们应该对使用返回值的情景和使用异常的情景进行区分,使用返回值来表达函数的状态是不推荐的,会导致上层编码风格的混乱

  2. 只使用返回值来传递数据,如果函数没有想要返回的值,尽量不要在函数中使用return,python会默认返回None

  3. 使用具体的异常类型,比如built-in的 ValueError, AttributeError, 不要使用 Exception, 如果需要自定义异常,将自定义的异常统一放到一个模块中,这样上层代码能方便访问你的自定义异常

  4. 尽量统一在上层处理异常,中间层尽量不处理异常,让异常扩散到统一处理异常的地方

Python语言规范 #异常

只处理应该处理的异常

不是所有异常你都可以处理的,许多异常你就应该把它抛出到调用者去,停止当前处理流程,沿着调用栈一层一层往外扔,直到这个exception被接住。如果你捕获住一个异常不往外抛,你就等于告诉调用方前面的过程没问题。使用者并不知情,直到出现更严重的后果才知道出现了问题,难以定位分析。

import requests

def get_html(url):
    try:
        return requests.get(url).text
    except Exception as e:
        print(e)
        return None
        # return ''

KEEP IT SIMPLE AND STUPID, DON'T BE TOO SMART

并不是把异常捕获然后 print / log 打印出来,就算是异常处理。

再举个例子:我们常用的一个包requests

import requests

url = 'www.baidu.com'

response = requests.get(url)

# 假设requests此时返回了一个None而不是抛出相应异常信息。
>>> response = requests.get(url)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/key/anaconda3/lib/python3.6/site-packages/requests/api.py", line 72, in get
    return request('get', url, params=params, **kwargs)
  File "/home/key/anaconda3/lib/python3.6/site-packages/requests/api.py", line 58, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/key/anaconda3/lib/python3.6/site-packages/requests/sessions.py", line 494, in request
    prep = self.prepare_request(req)
  File "/home/key/anaconda3/lib/python3.6/site-packages/requests/sessions.py", line 437, in prepare_request
    hooks=merge_hooks(request.hooks, self.hooks),
  File "/home/key/anaconda3/lib/python3.6/site-packages/requests/models.py", line 305, in prepare
    self.prepare_url(url, params)
  File "/home/key/anaconda3/lib/python3.6/site-packages/requests/models.py", line 379, in prepare_url
    raise MissingSchema(error)
requests.exceptions.MissingSchema: Invalid URL 'www.baidu.com': No schema supplied. Perhaps you meant http://www.baidu.com?

自定义异常

定义好包里面的可能出现的异常,异常的名字必须清晰而且有具体的意思,表示异常发生的问题。出现不能处理的一些情况,抛出异常可能会更好。

# requests/exceptions.py 截取部分
class RequestException(IOError):
    """There was an ambiguous exception that occurred while handling your
    request.
    """

    def __init__(self, *args, **kwargs):
        """Initialize RequestException with `request` and `response` objects."""
        response = kwargs.pop('response', None)
        self.response = response
        self.request = kwargs.pop('request', None)
        if (response is not None and not self.request and
                hasattr(response, 'request')):
            self.request = self.response.request
        super(RequestException, self).__init__(*args, **kwargs)


class MissingSchema(RequestException, ValueError):
    """The URL schema (e.g. http or https) is missing."""

重新抛出异常

你在 try/except 块中捕获了一个异常,处理完之后,你想重新把这个异常抛出。

try:
    0/0
except ZeroDivisionError:
    print('0除错误')
    raise

使用 raise from 语句来代替 raise 语句,它会让你同时保留两个异常的信息。

i = '十一'
try:
    int(i)
except ValueError:
    # raise MyException('参数错误,{}不能转化成整数'.format(i))
    raise MyException('参数错误,{}不能转化成整数'.format(i)) from e

提供命令行入口

simple
├── cli.py
├── core.py
├── exceptions.py
├── helpers.py
├── __init__.py
└── __main__.py
  • python -m simple
  • python -m simple.cli

结构化您的工程

构建可扩展、可测试、可靠的代码。

如果您的仓库的目录是一团糟,没有清晰的结构,导致协作和他人上手很困难,项目进度也会受到影响。

一个仓库样例

Github上有一个示例

docs/                    # 详细说明
sample/                  # 包,是这个仓库的核心
tests/                   # 测试用例
MANIFEST.in              # 文件清单
README.rst               # 项目简单介绍、如何快速上手、如何安装,配置,运行
requirements.txt         # 依赖文件
setup.py                 # setup

一个README示例 nocode

可以参考一些优秀项目的组织结构requestskerasflask

测试您的代码

编写测试代码将会帮助您更加精确地定义代码的含义,也给使用者展示了如何使用你的代码。

测试最重要的其实是检验程序的架构和设计,而不是单纯为了测试而测试。没有测试的代码给改bug、添加功能或重构埋下不少隐患。

难以测试的代码往往是设计不太好的代码,耦合严重。

  • 自动化测试
    • unitest
    • doctest
    • pytest
    • tox
    • nose
tests/
  __init__.py
  test_foo.py
# test_foo.py

import unittest
from simple import foo


class FooTestCase(unittest.TestCase):

    def test_foo(self):
        self.assertEqual(foo(), None)
  • python -m unittest tests.testfoo
  • python -m unitest discover -s tests

doctest

Doctest 将读取所有内嵌的看起来像Python命令行输入(以“>>>”为前缀)的文档字符串, 并运行,以检查命令输出是否匹配其下行内容。

这允许开发人员在源码中嵌入真实的示例和函数的用法。

# mymath.py
def square(x):
    """返回 x 的平方。

    >>> square(2)
    4
    >>> square(-2)
    4
    """

    return x * x

python -m doctest mymath.py -v

打包并发布

  • setuptools

将你的代码打包之后,有这些好处:

  1. 可以使用 pip or easy_install 安装.

  2. 可以做为其他包的依赖关系.

  3. 其他用户更加方便地使用和测试你的代码.

  4. 其他用户可以更方便的理解你的代码,因为你的代码是按照打包需要的格式来组织的.

  5. 更加方便添加和分发文档.

# setup.py
from setuptools import setup, find_packages


with open('README.rst') as f:
    readme = f.read()

with open('LICENSE') as f:
    license = f.read()

setup(
    name='sample',
    version='0.1.0',
    description='Sample package for Python-Guide.org',
    long_description=readme,
    author='Kenneth Reitz',
    author_email='me@kennethreitz.com',
    url='https://github.com/kennethreitz/samplemod',
    license=license,
    packages=find_packages(exclude=('tests', 'docs'))
)
  • name: 包名称,import name 被导入的名字
  • version: 版本号
  • description: PyPI首页的一句话短描述
  • long_description: PyPI首页的正文
  • url: 项目主页, 通常是项目的Github主页
  • license: 版权协议名
  • packages: 你要安装的包

或许你还是觉得太复杂或者太麻烦,这里还有个现成的

setup.py (for humans)

文档

既然项目做出来是给人用的,那文档就显得至关重要了。

  • sphinx 最流行的Python文档工具。

标记语言

文档字符串

像 Sphinx 这样的工具会将您的文档字符串解析为 reStructuredText,并以HTML格式正确呈现。

这使得在示例代码片段中嵌入项目的文档非常简单。

在Python中, 文档字符串 用来描述模块、类和函数:

def foo():
    """打印foo"""
    print('foo')

print(foo.__doc__)
help(foo)
# 打印foo

不要使用三引号去注释代码。

ReadTheDocs是一个在线文档托管服务,可以托管 Sphinx 文档。

你可以从各种版本控制系统配置 webhooks, 那么每次提交代码后可以自动构建并上传至readthedocs网站,非常方便。

持续集成(CI)

  • Jenkins
  • Travis

管理你的Python环境

创建虚拟环境通常是为了安装和管理第三方包。在系统提供的Python环境下,开发的项目越多、安装依赖越多,就可能会出现依赖冲突的问题。

为了解决这个问题,可以使用虚拟环境。分别为每个项目创建虚拟环境,这样就能与其他项目独立开来,保持环境的干净。

日常使用就这几个操作

  1. 创建一个虚拟环境
  2. 激活/进入虚拟环境
  3. 安装依赖,运行程序
  4. 退出虚拟环境
  5. 删除虚拟环境