Python 中的习语和反习语 — Python 文档

来自菜鸟教程
Python/docs/2.7/howto/doanddont
跳转至:导航、​搜索

Python 中的习语和反习语

作者
摩西·扎德卡

本文档被置于公共领域。

摘要

本文档可被视为本教程的配套文件。 它展示了如何使用 Python,更重要的是,展示了如何 not 使用 Python。


你不应该使用的语言结构

尽管与其他语言相比,Python 的问题相对较少,但它仍然有一些结构仅在极端情况下有用,或者是非常危险的。

从模块导入 *

内部函数定义

from module import * 在函数定义中是 invalid。 虽然许多版本的 Python 不检查无效性,但它并没有使它更有效,就像拥有一个聪明的律师使一个人变得无辜一样。 永远不要那样使用它。 即使在它被接受的版本中,它也会使函数执行速度变慢,因为编译器无法确定哪些名称是本地的,哪些是全局的。 在 Python 2.1 中,此构造会导致警告,有时甚至会导致错误。


在模块级别

虽然在模块级别使用 from module import * 是有效的,但通常是一个坏主意。 首先,这失去了 Python 原本拥有的一个重要属性——您可以通过您最喜欢的编辑器中的简单“搜索”功能知道每个顶级名称的定义位置。 如果某个模块增加了额外的函数或类,你将来也会遇到麻烦。

在新闻组上提出的最糟糕的问题之一是为什么这段代码:

f = open("www")
f.read()

不起作用。 当然,它工作得很好(假设您有一个名为“www”的文件。)但是如果模块中的某个地方存在语句 from os import *,则它不起作用。 os 模块有一个名为 open() 的函数,它返回一个整数。 虽然它非常有用,但隐藏内置函数是其最不实用的属性之一。

请记住,您永远无法确定模块导出的名称,因此要么获取您需要的名称 — from module import name1, name2,要么将它们保留在模块中并根据需要访问 — import module;print module.name


当它很好的时候

在某些情况下 from module import * 就好了:

  • 交互式提示。 例如,from math import * 使 Python 成为一个了不起的科学计算器。
  • 使用 Python 中的模块扩展 C 中的模块时。
  • 当模块宣称自己为 from import * 安全时。


未修饰的 exec、execfile() 和朋友

“未修饰”一词是指在没有明确字典的情况下使用,在这种情况下,这些构造评估 current 环境中的代码。 这很危险,原因与 from import * 是危险的一样——它可能会越过你指望的变量,并把其余代码的事情搞砸。 不要那样做。

不好的例子:

>>> for name in sys.argv[1:]:
>>>     exec "%s=1" % name
>>> def func(s, **kw):
>>>     for var, val in kw.items():
>>>         exec "s.%s=val" % var  # invalid!
>>> execfile("handler.py")
>>> handle()

很好的例子:

>>> d = {}
>>> for name in sys.argv[1:]:
>>>     d[name] = 1
>>> def func(s, **kw):
>>>     for var, val in kw.items():
>>>         setattr(s, var, val)
>>> d={}
>>> execfile("handle.py", d, d)
>>> handle = d['handle']
>>> handle()

从模块导入名称1,名称2

这是一个“不要”,它比之前的“不要”要弱得多,但如果你没有充分的理由这样做,你仍然不应该这样做。 它通常是一个坏主意的原因是因为您突然拥有一个位于两个独立命名空间中的对象。 当一个命名空间中的绑定发生变化时,另一个命名空间中的绑定不会发生变化,因此它们之间会存在差异。 例如,当重新加载一个模块或在运行时更改函数的定义时,就会发生这种情况。

不好的例子:

# foo.py
a = 1

# bar.py
from foo import a
if something():
    a = 2 # danger: foo.a != a

好的例子:

# foo.py
a = 1

# bar.py
import foo
if something():
    foo.a = 2

除了:

Python 有 except: 子句,它可以捕获所有异常。 由于 Python 中的 every 错误会引发异常,因此使用 except: 会使许多编程错误看起来像运行时问题,从而阻碍调试过程。

以下代码显示了为什么这是不好的一个很好的例子:

try:
    foo = opne("file") # misspelled "open"
except:
    sys.exit("could not open file!")

第二行触发了一个 NameError,它被 except 子句捕获。 程序会退出,程序打印的错误信息会让你认为问题是"file"的可读性,而实际上真正的错误与"file"无关。

写上面的更好的方法是

try:
    foo = opne("file")
except IOError:
    sys.exit("could not open file")

当它运行时,Python 将产生一个显示 NameError 的回溯,并且很明显需要修复什么。

因为 except: 捕获 all 异常,包括 SystemExitKeyboardInterruptGeneratorExit(这不是错误,通常不应被用户代码捕获),使用裸 except: 几乎从来都不是一个好主意。 在需要捕获所有“正常”错误的情况下,例如在运行回调的框架中,您可以捕获所有正常异常的基类 Exception。 不幸的是,在 Python 2.x 中,第三方代码可能会引发不继承自 Exception 的异常,因此在 Python 2.x 中,在某些情况下您可能不得不使用裸 except: 并手动重新引发您不想捕获的异常。


例外

异常是 Python 的一个有用特性。 你应该学会在发生意外时提出他们,并只在你可以做些什么的地方抓住他们。

下面是一个非常流行的反成语

def get_status(file):
    if not os.path.exists(file):
        print "file not found"
        sys.exit(1)
    return open(file).readline()

考虑在调用 os.path.exists() 和调用 open() 之间文件被删除的情况。 在这种情况下,最后一行将引发 IOError。 如果 file 存在但没有读取权限,也会发生同样的事情。 由于在普通机器上对存在和不存在的文件进行测试使其看起来没有错误,因此测试结果看起来不错,并且代码将被交付。 后来一个未处理的 IOError(或者可能是其他一些 EnvironmentError)逃到用户那里,用户可以看到丑陋的回溯。

这是一个更好的方法。

def get_status(file):
    try:
        return open(file).readline()
    except EnvironmentError as err:
        print "Unable to open file: {}".format(err)
        sys.exit(1)

在此版本中, 或者 文件被打开并读取行(因此它甚至可以在不稳定的 NFS 或 SMB 连接上工作),或者打印一条错误消息,提供有关打开失败原因的所有可用信息,并且应用程序被中止。

然而,即使是这个版本的 get_status() 也做出了太多的假设——它只会在运行时间较短的脚本中使用,而不是在长时间运行的服务器中使用。 当然,调用者可以做类似的事情

try:
    status = get_status(log)
except SystemExit:
    status = None

但是有更好的方法。 你应该尽量在你的代码中使用尽可能少的 except 子句——你使用的那些通常在内部调用中应该总是成功,或者在主函数中包含所有内容。

所以,一个更好的 get_status() 版本可能是

def get_status(file):
    return open(file).readline()

调用者可以根据需要处理异常(例如,如果它在循环中尝试多个文件),或者只是让异常向上过滤到 its 调用者。

但是最后一个版本仍然有一个严重的问题——由于 CPython 中的实现细节,在异常处理程序完成之前,当引发异常时文件不会关闭; 更糟糕的是,在其他实现(例如 Jython)中,无论是否引发异常,它都可能根本不会关闭。

此函数的最佳版本使用 open() 调用作为上下文管理器,这将确保在函数返回后立即关闭文件:

def get_status(file):
    with open(file) as fp:
        return fp.readline()

使用电池

每隔一段时间,人们似乎又在 Python 库中编写东西,通常很糟糕。 虽然偶尔模块的接口很差,但使用 Python 附带的丰富的标准库和数据类型通常比自己发明要好得多。

一个很少有人知道的有用模块是 os.path。 它始终为您的操作系统提供正确的路径算法,并且通常比您自己想出的方法要好得多。

相比:

# ugh!
return dir+"/"+file
# better
return os.path.join(dir, file)

os.path 中更多有用的函数:basename()dirname()splitext()

还有许多有用的内置函数,人们似乎出于某种原因没有意识到: min()max() 可以找到具有可比性的任何序列的最小值/最大值例如语义,但许多人自己编写 max()/min()。 另一个非常有用的函数是 reduce(),它可用于对序列重复应用二元运算,将其减少为单个值。 例如,用一系列乘法运算计算阶乘:

>>> n = 4
>>> import operator
>>> reduce(operator.mul, range(1, n+1))
24

在解析数字时,请注意 float()int()long() 都接受字符串参数,并将拒绝格式错误的字符串举起 ValueError


使用反斜杠继续语句

由于 Python 将换行符视为语句终止符,并且由于语句通常放在一行中并不容易,因此许多人会这样做:

if foo.bar()['first'][0] == baz.quux(1, 2)[5:9] and \
   calculate_number(10, 20) != forbulate(500, 360):
      pass

您应该意识到这是危险的:\ 后面的杂散空格会使这一行出错,而且杂散空格在编辑器中是出了名的难以看到。 在这种情况下,至少会出现语法错误,但如果代码是:

value = foo.bar()['first'][0]*baz.quux(1, 2)[5:9] \
        + calculate_number(10, 20)*forbulate(500, 360)

那么它只是微妙的错误。

使用括号内的隐式延续通常要好得多:

这个版本是防弹的:

value = (foo.bar()['first'][0]*baz.quux(1, 2)[5:9]
        + calculate_number(10, 20)*forbulate(500, 360))