文档章节

践行这五条原则,构建优秀的Python包

yehun
 yehun
发布于 2017/03/30 17:14
字数 1901
阅读 9
收藏 1

本文作者为 mssaxm,译者为 liubj2016,校对 EarlGrey,是 Python 翻译组 推出的最新译文。本文为编程派微信公众号首发。

构建一个包貌似很简单,只要把一堆模块都放进一个有 __init__.py 文件的目录里面就行了,对吧?可能看上去简单粗暴,但是随着对包的修改越来越多,设计不好的包就会产生循环依赖问题,而且会变得臃肿、脆弱。
遵循这五个简单的原则,有助于避免这些常见的坑,让你的包能够使用的更久,变得更强大。

1. __init__.py 只是用来导入包

如果是一个简单的包,你可能会很想把辅助方法、工厂和异常,一股脑都丢进 __init__.py 中。千万不要这样做。
格式良好的__init__.py有一个重要作用:导入子模块。你的 __init__.py 应该像这样:

# 导入顺序很重要 —— 有些模块依赖于其他模块
from exceptions import FSQError, FSQEnvError, FSQEncodeError,\
                       FSQTimeFmtError, FSQMalformedEntryError,\
                       FSQCoerceError, FSQEnqueueError, FSQConfigError,\
                       FSQPathError, FSQInstallError, FSQCannotLockError,\
                       FSQWorkItemError, FSQTTLExpiredError,\
                       FSQMaxTriesError, FSQScanError, FSQDownError,\
                       FSQDoneError, FSQFailError, FSQTriggerPullError,\
                       FSQHostsError, FSQReenqueueError, FSQPushError

# constants 依赖于:exceptions,internal
import constants

# const 依赖于:constants,exceptions,internal
from const import const, set_const # has tests

# path 依赖于:exceptions,constants,internal
import path # has tests

# lists 依赖于:path
from lists import hosts, queues

#...

2. 用 __init__.py 限制导入的顺序

在上面的例子中,__init.py 解决了两个问题:
在包的作用域中暴露方法和类,用户不必深入到包的内部结构,即可轻松使用包。
协调导入顺序的唯一位置。
运用得好的话,__init.py 可以让你灵活地再组织包的内部结构,而不需要担心内部子模块导入或每个模块的导入顺序带来的副作用。由于你按照某种特定的顺序导入子模块,你的__init__.py可以很容易被其他程序员理解,并且可以说明该包所提供的功能。

在包这一级,一个文档字符串以及 __all__ 属性赋值,就是你的__init__.py中唯一的非导入代码:

__all__ = [ 'FSQError', 'FSQEnvError', 'FSQEncodeError', 'FSQTimeFmtError',
            'FSQMalformedEntryError', 'FSQCoerceError', 'FSQEnqueueError',
            'FSQConfigError', 'FSQCannotLock', 'FSQWorkItemError',
            'FSQTTLExpiredError', 'FSQMaxTriesError', 'FSQScanError',
            'FSQDownError', 'FSQDoneError', 'FSQFailError', 'FSQInstallError',
            'FSQTriggerPullError', 'FSQCannotLockError', 'FSQPathError',
            'path', 'constants', 'const', 'set_const', 'down', 'up',
            # ...
          ]

3. 用一个模块定义所有的异常

你可能已经注意到了,在__init__.py的开头,通过一个单一的子模块 exceptions.py 导入了所有的异常。这与在大多数包中看到的不同,大多数的包会在抛出异常的代码附近来定义异常。虽然这可以使得模块更加紧密,但是当包足够复杂的时候,则会出现问题,有下面两种情况:

通常,模块/程序需要导入一个子模块来获取一个函数,这个函数可以导入并且使用抛出异常的代码。为了捕获更高粒度的异常,你需要同时导入你需要的模块以及定义了异常的模块(或者更糟,需要链式导入异常)。这种衍生出来的导入要求,只是将你的包中的导入关系变复杂的第一步。你使用这种模式的次数越多,包的相互依赖性就越强,更容易出错。
随着异常越来越多,找到所有包能够抛出的错误会越来越难。在一个模块中定义所有的异常,可以让程序员轻易地检查确定你的包抛出所有潜在错误情况。
你应该在包中定义一个基类异常:

class APackageException(Exception):
    '''root for APackage Exceptions, only used to except any APackage error, never raised'''
    pass

然后,确保在所有错误情况下,你的包抛出的异常都是这个基类异常的子类,这样如果你需要的话,就可以禁止所有的异常:

try:
    '''bunch of code from your package'''
except APackageException:
    '''blanked condition to handle all errors from your package'''

对于一些通用的错误情况,已经在标准库中包含了想要的异常(比如 TypeError、ValueError等)。
定义足够多的异常,并且要有充足的颗粒度:

# from fsq
class FSQEnvError(FSQError):
    '''An error if something cannot be loaded from env, or env has an invalid
       value'''
    pass

class FSQEncodeError(FSQError):
    '''An error occured while encoding or decoding an argument'''
    pass
# ... and 20 or so more

异常的粒度越高,程序员就可以使用 try / except,包裹住越大的代码块:

# 像这样
try:
   item = fsq.senqueue('queue', 'str', 'arg', 'arg')
   scanner = fsq.scan('queue')
except FSQScanError:
   '''do something'''
except FSQEnqueueError:
   '''do something else'''

# 而不是这样
try:
    item = fsq.senqueue('queue', 'str', 'arg', 'arg')
except FSQEnqueueError:
    '''do something else'''
try:
    scanner = fsq.scan('queue')
except FSQScanError:
    '''do something'''

# 千万不要这样
try:
    item = fsq.senqueue('queue', 'str', 'arg', 'arg')
    try:
        scanner = fsq.scan('queue')
    except FSQScanError:
        '''do something'''
except FSQEnqueueError:
    '''do something else'''

异常定义中的高粒度,使得错误处理更简单易懂,并且可以将常规指令和错误操作指令分组归类,使代码变得容易理解和维护。

4. 在包中只进行相对导入

在子模块中最容易犯的错误就是,使用包自身的名字来导入包:

# within a sub-module
from a_package import APackageError

这一语句会导致如下两种不好的结果:

  • 只有当这个包安装在 python 环境变量路径 PYTHONPATH 中的时候,这个子模块才会正常运行。
  • 只有当包的名字是 a_package 的时候,这个子模块才会正常运行。

第一条好像不是什么大问题,但是如果你的环境变量路径中不同的目录下安装了两个同名的包,你的子模块可能会导入另一个包,你无意间的失误将会让程序员(或者就是你自己)调试很久。与其使用你自己的包的名字,不如在包中采用相对导入:

# within a sub-module
from . import FSQEnqueueError, FSQCoerceError, FSQError, FSQReenqueueError,\
              constants as _c, path as fsq_path, construct,\
              hosts as fsq_hosts, FSQWorkItem
from . internal import rationalize_file, wrap_io_os_err, fmt_time,\
                      coerce_unicode, uid_gid
# you can also use ../... etc. in sub-packages.

5. 保持模块小巧

模块应该尽量小巧。记住,程序员在使用你的包时,将会从包的作用域中导入,而你可以使用 __init__.py 作为一个管理工具,连贯地暴露接口。
一个很好的经验是,在每个模块中只定义一个类,以及所需要的任何辅助方法和工厂方法:

class APackageClass(object):
    '''One class'''

def apackage_builder(how_many):
    for i in range(how_many):
        yield APackageClass()

如果模块中有要暴露出来的方法,那么就将相互依赖的方法放到一个模块中,将不相互关联的方法移到其他模块:

####### EXPOSED METHODS #######
def enqueue(trg_queue, item_f, *args, **kwargs):
    '''Enqueue the contents of a file, or file-like object, file-descriptor or
       the contents of a file at an address (e.g. '/my/file') queue with
       arbitrary arguments, enqueue is to venqueue what printf is to vprintf
    '''
    return venqueue(trg_queue, item_f, args, **kwargs)

def senqueue(trg_queue, item_s, *args, **kwargs):
    '''Enqueue a string, or string-like object to queue with arbitrary
       arguments, senqueue is to enqueue what sprintf is to printf, senqueue
       is to vsenqueue what sprintf is to vsprintf.
    '''
    return vsenqueue(trg_queue, item_s, args, **kwargs)

def venqueue(trg_queue, item_f, args, user=None, group=None, mode=None):
    '''Enqueue the contents of a file, or file-like object, file-descriptor or
       the contents of a file at an address (e.g. '/my/file') queue with
       an argument list, venqueue is to enqueue what vprintf is to printf
       if entropy is passed in, failure on duplicates is raised to the caller,
       if entropy is not passed in, venqueue will increment entropy until it
       can create the queue item.
    '''
    # setup defaults
    trg_fd = name = None
    # ...

上面的例子 fsq/enqueue.py,暴露了一组函数,它们提供了同一功能的不同接口(类似于 simplejson 中的 load/loads)。虽然这个例子很简单直白,但是要做到保持模块小巧,需要一定的判断力,不过一个很少的做法是:
如不确定,则新建模块。

 

© 著作权归作者所有

yehun
粉丝 8
博文 218
码字总数 137315
作品 0
长宁
高级程序员
私信 提问
youtube-dl 的跨平台前端 GUI - youtube-dlG

youtube-dlG,流行的 youtube-dl 的跨平台前端 GUI,是用 wxPython 所写的,wxPython 是 Python 语言的一套优秀的 GUI 图形库。 要求: Python 2.7.3+ wxPython 3 woDict GNU gettext(用于构...

匿名
07/02
506
0
Pythonic到底是什么玩意儿?

这是几个月前在 EuroPython 邮件列表(主要用来组织和计划 EuroPython 会议的邮件列表)出现的问题。这是一个非常有意思的问题,我看到这个词被无数次地使用,但鲜有人尝试解释它的含义。在这...

陶邦仁
2013/01/15
436
2
Python 机器学习的必备技巧

尝试使用 Python 掌握机器学习、人工智能和深度学习。 想要入门机器学习并不难。除了大规模网络公开课Massive Open Online Courses(MOOC)之外,还有很多其它优秀的免费资源。下面我分享一些...

作者: Tirthajyoti Sarkar
2018/11/08
0
0
Python为什么是编程语言中最skr的?

Python的出现让计算机编程语言不再是生僻的专业技能,而是常人都能学习和使用的万金油。 《经济学人(Economist)》近日对Python的一篇专题报道,揭秘了这一把计算机思维带入寻常百姓家的神奇...

weixin_43932460
01/07
0
0
在 OpenStack 中启用 DB2

OpenStack 是一个云操作系统,它控制着整个数据中心中庞大的计算、存储和网络资源池。所有资源都通过一个仪表板来进行管理,这为管理员提供了控制权,同时使用户能够通过 Web 界面配制资源。...

IBMdW
2013/01/15
495
1

没有更多内容

加载失败,请刷新页面

加载更多

前端面试题汇总

一. HTML常见的兼容性 1.HTML5 标签在低版本浏览器不兼容 解决办法:使用html5shiv库,引入下列语句 <!--[if lte IE 8]> <script src="https://cdn.bootcss.com/html5shiv/r29/html5.js"></sc......

蓝小驴
37分钟前
10
0
OSChina 周四乱弹 —— 我气的脸都黑了!

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 小小编辑推荐《Red Battle》- 高橋李依 / 豊崎愛生 《Red Battle》- 高橋李依 / 豊崎愛生 手机党少年们想听歌,请使劲儿戳(这里) @丶Lion ...

小小编辑
50分钟前
653
22
找OSG教程, B站就有

https://www.bilibili.com/video/av64849038?from=search&seid=11632913960900279653

洛克人杰洛
今天
6
0
学习记录(day07-Vue组件、自定义属性、自定义事件)

[TOC] 1.1.1什么是组件 一个vue文件就是一个组件 组件将html标签/css样式/对应JS打包成一个整体,也可以理解钻进一个具有样式和特效的自定义标签。 一、编写组件(提供方)<template> <di...

庭前云落
今天
5
0
使用Prometheus监控SpringBoot应用

通过之前的文章我们使用Prometheus监控了应用服务器node_exporter,数据库mysqld_exporter,今天我们来监控一下你的应用。(本文以SpringBoot 2.1.9.RELEASE 作为监控目标) 编码 添加依赖 使...

JAVA日知录
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部