Python专家一样编程: 地道的 Python 重新排版整理

Posted by JC on 2015-07-07 14:48:37 Updated on 2015-07-07 14:48:37

Python专家一样编程: 地道的 Python 重新排版整理,这个小册子虽然很老了,但里面的内容都很好,值得反复阅读。

像 Python专家⼀样编程: 地道的 Python

David Goodger goodger@python.org http://python.net/~goodger

翻译: 令狐⾍虫 ch.linghu@gmail.com

本文原文:http:// python.net/~goodger/projects/pycon/2007/idiomatic/handout.html

在这个交互教程中,我们会探讨很多基本 Python 惯用法及其深层技术, 让你⽴即拥有⼀些有用的 ⼯具。 本演⽰有 3 个版本: S5 幻灯⽚ HTML 页⾯ reStructuredText 源代码

©2006-2008, licensed under a Creative Commons Attribution/Share-Alike (BY-SA) license.

本⼈简历【译注:指作者本人】: 我是

  • 蒙特利尔⼈,
  • 两个优秀孩子的⽗亲,⼀个特别⼥人的丈夫,
  • 全职 Python 程序员,
  • Docutils 项⽬和 reStructuredText 的作者,
  • Python 改善建议(即 PEPs)的编辑,
  • PyCon 2007 组织者, PyCon 2008 主席,
  • Python 软件基⾦会成员,
  • 基⾦会过去一年的董事及秘书。 在 PyCon 2006 我演⽰⼀个教程(名字叫⽂本及数据处理 (Text & Data Processing)) 时,对于我使用的⼀些技术和我所认为的 ⼀些常识所得到的反应,我感到十分惊讶。这些有经验的 Python 程序员不假思索在使⽤的工具,很多与会者根本就不知道。 你们中的很多⼈应该之前已经了解了这些技术和惯⽤用法中的⼀部分。希望你们可以学到⼀些以前不知道的技术,或者在已经知道的那部分里学到些新东西。

目录

  • Python之禅 (1)
  • Python之禅 (2)
  • 代码风格:可读性至上
  • PEP 8: Style Guide for Python Code
  • 空格 1
  • 空格 2
  • 命名
  • 长代码行 & 续行
  • 长字符串
  • 复合语句
  • Docstrings 和注释
  • 实用性大于纯粹性
  • 值交换
  • 关于 tuple 的更多
  • 交互模式下的 "_"
  • 通过子字符串构建字符串
  • 构建字符串, 变化 1
  • 构建字符串, 变化 2
  • 可能的话就用 in (1)
  • 可能的话就用 in (2)
  • 字典的 get 方法
  • 字典的 setdefault 方法 (1)
  • 字典的 setdefault 方法 (2)
  • defaultdict
  • 构建和拆分字典
  • 真值测试
  • 真值
  • 索引和项 (1)
  • 索引和项 (2): enumerate
  • 其它语言拥有「变量」
  • Python拥有「名称」
  • 参数默认值
  • % 字符串格式化
  • 高级 % 字符串格式化
  • 高级 % 字符串格式化
  • 列表推导
  • 生成器表达式(1)
  • 生成器表达式 (2)
  • 排序
  • 使用 DSU 进行排序 *
  • 按键值排序
  • 生成器
  • 生成器举例
  • 从文本/数据文件中按行读取
  • EAFP vs. LBYL
  • EAFP风格的 try/except 举例
  • 导入
  • 模块和脚本
  • 模块结构
  • 命令行处理
  • 简单好过复杂
  • 不要重新发明轮子
  • 参考文献

Python之禅 (1)

这是 Python的指导性原则,但其解释是开放性的。想要正确解释它们, 需要点儿幽默感。

如果你正在用一门以小品喜剧剧团命名的编程语言,你最好有点儿幽默感。

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.

   ...

Python之禅 (2)

In the face of ambiguity, refuse the temptation to guess.
There should be oneand preferably only oneobvious 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 idealet's do more of those!
Tim Peters

这首特别的「诗」以某种玩笑开头,不过它确实揭示了很多 python 背后哲学的真相。 「 Python 之禅」已经正式进入了 PEP 20,它的摘要里说:

元老级 Python 先锋 Tim Peters 将 Python 设计的 BDFL 指导性原则缩减成 20 条格言, 只有其中19条被写了下来。 — http://www. python.org/dev/peps/pep-0020/

你可以想想你是一个「 Python 先锋」还是一个「 Python 专家」。这两个术语有些不一样的内涵。

每当心存疑虑时:

import this

试试在 python交互环境中输入:

>>> import this

这是另一个彩蛋:

>>> from __future__ import braces
File "<stdin>", line 1
SyntaxError: not a chance

【译注:上面这段意思是想在特性中引入大括号,会出来一个语法异常:没机会了。 其内涵不用解释了吧】

真是一帮子喜剧演员! :-)

代码风格:可读性至上

程序是写来给人读的,只是偶尔让机器去执行。--Abelson & Sussman, 计算机程序的构造与解释

尽量让你的程序一目了然,易于阅读。

PEP 8: Style Guide for Python Code

Worthwhile reading:

http://www. python.org/dev/peps/pep-0008/

PEP = python Enhancement Proposal

PEP是一种设计文档,用于向 python社区提供信息,或者是用来描述 Python 语言或其处理或其环境的新特性。

python社区对于源代码看起来应该怎么样有自己的标准,规范在 PEP 8 中。 这个标准不同于其它社区,比如 C、C++、C#、Java、VisualBasic,等等。

由于缩进和空格对于 Python 如此重要, Python 代码的风格指南已经基本上算是标准了。跟着指南走才是聪明的做法!大多数的开源项目和内部项目(希望如此) 非常接近的遵守风格指南。

空格 1

  • 每个缩进层次使用 4 个空格.
  • 不要使用硬 tab【译注:即 '\t' 字符】.
  • 绝对不要混用 tab 和空格.
  • 这正是 IDLE 和 Emacs 的 Python 模式所支持的。其它的编辑器也应该提供这样的支持。
  • 函数和函数之间空一行.
  • 类和类之间空两行.

空格 2

  • 在 dict、list、tuple 和参数列表的「,」后面,dict 的「:」后面加一个空格, 但前面不要加
  • 在赋值符和关系运算符的前后加空格(参数列表里的不用)
  • 紧邻括号的内部,也就是参数列表的最前面,不加空格
  • 紧邻 docstrings 的内部不加空格
def make_squares(key, value=0):
    """Return a dictionary and a list..."""
    d = {key: value}
    l = [key, value]
    return d, l

命名

  • 函数、方法、属性用小写+下划线形式(joined_lower)
  • 常量用小写+下划线形式(joined_lower)或大写+下划线形式(ALL_CAPS)
  • 类用首字母大写形式(StudlyCaps)
  • 驼峰命名法(camelCase)仅仅用于符合事先存在的约定
  • 属性: 接口(公开属性) interface , 内部属性 _internal , 私有属性 __private 但是尽量避免使用私用属性 __private 的形式。我从来没用过它。 相信我。如果你用它,你以后一定会后悔的。

解释: 来自 C++/Java 背景的人特别容易过度使用/错误使用这个「特性」。但私有命名 __private 并不像 Java 或 C++ 里那样工作。它们只是触发了一个 命名混淆其目的是防止在子类中发生命名空间的冲突: MyClass.__private 会变成 MyClass._MyClass__private 。(注意,当基类和子类名字相同时,这种方法 会失效。比如,子类在另外一个模块的时候。)在类的外部存取一个私有属性是 完全可能的,只是不太方便和靠谱(它增加了对基类名字的精确依赖)。 问题在于,一个类的作者会合理的认为「这个属性/方法名字应该是私有的, 只能被这个类定义本身存取」然后使用了私有属性 __private 的约定。 但是后来,使用该类派生子类的用户会很合理的需要存取这个名字。所以 要么去改父类(一般很难甚至不可能),要么就在子类里用手工混淆的名字(难看并且 不靠谱)。 在 python 里有一个概念:「在这里我们都是成年人」。如果你使用了私有属性 形式,你在保护谁的属性?对于子类来说,更可取的方法是正确的使用 来自父类的属性,父类把它自身的属性文档写正确才是可取之道。 更好的方法是使用单下划线约定 _internal 表示内部属性。 这完全不会触发命名混淆,它只是在告诉别人「小心这个,这是一个内部实现细节。 如果你没有完全理解它,不要碰它。当然,这纯粹只是一个约定而已。 关于这个问题,这里有一些很好的解释:

长代码行 & 续行

保持每行长度不超过80字符。

在括号/方括号/大括号里使用隐式续行:

def __init__(self, first, second, third,
            fourth, fifth, sixth):
output = (first + second + third
         + fourth + fifth + sixth)

在最后加上反斜杠表示显式续行:

VeryLong.left_hand_side \
    = even_longer.right_hand_side()

反斜杠是脆弱的,它必须是一行的最后一个字符。如果你在反斜杠后面加上一个空格, 它就不再工作了。另外,它也是丑陋的。

长字符串

相邻的字符串文本会被语法分析器自动连接:

>>> print 'o' 'n' "e"
one

文本之间的空格不是必需的,但加空格可以提高可读性。 任何形式的引号都可以使用:

>>> print 't' r'\/\/' """o"""
t\/\/

字符串加一个「r」前缀表示「原生」字符串。反斜杠在原生字符串中不再解释为脱字符。 这在写正则表达式和Windows文件系统路径时很有帮助。

注意,字符串变量不会被连接:

>>> a = 'three'
>>> b = 'four'
>>> a b
File "<stdin>", line 1
       a b

SyntaxError: invalid syntax

那是因为这种自动连接是 Python 语法分析器/编译器的特性,而不是解释器的。 在运行时连接字符串,你必须使用「+」操作符。

text = ('Long strings can be made up '
        'of several shorter strings.')

括号允许隐式续行。

多行字符串用三引号:

"""Triple
double
quotes"""
'''\
Triple
single
quotes\
'''

上面的最后一个例子(三单引号),演示了如何使用反斜杠来避免换行。 它消除了额外的换行,并将文字和引号保持为漂亮的左对齐。 反斜杠必须是每行的最后一个符号。

复合语句

Good:

if foo == 'blah':
  do_something()
do_one()
do_two()
do_three()

Bad:

if foo == 'blah': do_something()
do_one(); do_two(); do_three()

空格和缩进可以非常有效的视觉化展现程序流程。上面「Good」第二行的 缩进就告诉了读者发生了什么事,而缺乏缩进的「Bad」则让「if」语句 变得不那么明显。

将多条语句写成一行是重罪。在 Python 里, 可读性至上。

Docstrings 和注释

Docstrings = 如何使用代码

注释 = 为什么 (解释) 以及代码是如何工作的

Docstrings 解释了如何去使用代码,它是给你代码的用户看的。 Docstrings 的使用:

  • 解释函数的目的,即使对你来说这不言自明,因为它也许对以后的其它人 来说并没有那么明显。
  • 描述期望接受的参数、返回值和可能产生的任何异常。
  • 如果这个方法跟一个调用者有紧密的绑定,对这个调用者做一些说明。 (但是要小心这个调用者以后可能会发生变化)

注释解释了为什么,是给你代码的维护者看的。 这里有一些例子,包括写给你自己的说明,比如:

# !!! BUG: ...

# !!! FIX: This is a hack

# ??? Why is this here?

这里的「用户」和「维护者」都包含了 你自己 ,所以,把 Docstrings 和注释写好点!

Docstrings 对于交互式使用(help())和自动文档工具来说很有用。

错误的注释和 docstrings 比没有还要更糟。所以,保持它们的更新!当你做了 变更,确保注释和docstrings 和代码保持一致,不要让它们产生矛盾。

这里有一个专门的关于 Docstring 的 PEP,PEP 257,"Docstring 约定"

http://www. python.org/dev/peps/pep-0257/

实用性大于纯粹性

A foolish consistency is the hobgoblin of little minds. 【译注:这句话里的 consisitency 带双关性,原意和这里的意思不一样,不好译,就不译了】 —Ralph Waldo Emerson

(hobgoblin: 导致迷信的恐惧的事情; 一个稻草人.)

永远都存在例外. PEP 8中说:

但是最重要的一点:知道何时不一致 —— 有时风格指南无法应用。在有 疑问时,使用你的最佳判断。查看其它的例子,然后决定哪个看起来最好。 另外,别迟疑,赶快问!

两个打破常规的好理由: 1. 当使用规则会让代码变得更难读,甚至对以前读过使用该规则代码 的人来说也是如此时。 2. 为了跟周围同样破坏规则的代码保持一致(通常是因为历史原因)。 —— 不过,这也是一个清理垃圾的机会(在真正的 XP 风格中)。

... 无论如何实用性不能把纯粹性打成一团肉酱!

值交换

在其它语言里:

temp = a
a = b
b = temp

在 Python里:

b, a = a, b

可能你以前已经见过。但是你知道它是如何工作的吗?

  • 逗号是 tuple 构造语法
  • 右边建立了一个 tuple (tuple packing).
  • 左边是一个目标 tuple (tuple unpacking).

右手边 解包(unpacked) 到左手边 tuple 里对应的名字

更多 unpacking 的例子:

>>> l =['David', ' pythonista', '+1-514-555-1234']
>>> name, title, phone = l
>>> name
'David'
>>> title
' pythonista'
>>> phone
'+1-514-555-1234'

对于循环处理结构数据很有用:

l (L) 是我们刚刚在上面建立的 (David 的信息)。所以 people 是一个包含两个项的 list,每个项都是一个包含3个项的 list。

>>> people = [l, ['Guido', 'BDFL', 'unlisted']]
>>> for (name, title, phone) in people:
...     print name, phone
...
David +1-514-555-1234
Guido unlisted

people 中的每一项都被 unpacked 到(name, title, phone)这个 tuple。

可以任意嵌套 (只要保证左右两边的结构匹配):

>>> david, (gname, gtitle, gphone) = people
>>> gname
'Guido'
>>> gtitle
'BDFL'
>>> gphone
'unlisted'
>>> david
['David', ' pythonista', '+1-514-555-1234']

关于 tuple 的更多

我们注意到,逗号是 tuple 的构造项,而不是括号。例如:

>>> 1,
(1,)

为了清晰起见, Python 解释器会显示括号。我也同样推荐你使用括号:

>>> (1,)
(1,)

不要忘记逗号!

>>> (1)
1

在单元素 tuple中,尾部的逗号是必需的;在两个及以上元素的 tuple 中, 尾部的逗号是可选的。0 元素的 tuple,也就是空 tuple,用一对括号来作为简化语法。

>>> ()
()
>>> tuple()
()

一个常见的错误就是在不需要 tuple 的地方留了一个逗号 【译注:于是变成了一个 tuple】 这会很轻易的让你的代码发生错误。

>>> value = 1,
>>> value
(1,)

所以如果你在意想不到的地方看到了一个 tuple,检查逗号!

交互模式下的 "_"

这真的是一个非常有用的特性。奇怪的是很少有人知道。

在交互式解释器里,当你对一个表达式求值或调用了一个函数,其结果会 存入一个临时名称, _ (一个下划线):

>>> 1 + 1
2
>>> _
2

_ 保存最后一个打印出来的结果

当结果是 None 的时候,什么也没有打印,所以 _ 不会发生变化。 这很方便!

这只在交互式解释器里有用,在 module 里没用。

当你交互式的解决一个问题,然后你想保存上一步的结果时,这个功能尤其方便。

>>> import math
>>> math.pi / 3
1.0471975511965976
>>> angle = _
>>> math.cos(angle)
0.50000000000000011
>>> _
0.50000000000000011

通过子字符串构建字符串

从一个字符串列表开始:

colors = ['red', 'blue', 'green', 'yellow']

我们想把全部这些字符串合并成一个大字符串。尤其是子字符串数量很大的时候... 别这样做:

result = ''
for s in colors:
    result += s

这么做非常的低效。

这是一个典型的恐怖内存占用和恐怖性能的范例。「合并结果」在 每一个中间步骤都会计算、存储然后被抛弃。

相反的,你应该这么做:

result = ''.join(colors)

字符串方法 join() 会一次性做完全部的复制动作。

当你只是处理几十甚至几百个字符串时,它们的差别不会很大。 但是要养成使用高效字符串构建方法的习惯, 因为当你处理上千个字符串,或者是在循环中进行处理, 它们确实有差别。

构建字符串, 变化 1

这里有一些使用字符串方法 join() 的技巧。 如果你想在每一个子字符串之间插入一个空格:

result = ' '.join(colors)

或者是逗号加空格:

result = ', '.join(colors)

这是一种常见情况:

colors = ['red', 'blue', 'green', 'yellow']
print 'Choose', ', '.join(colors[:-1]), \
     'or', colors[-1]

为了生成语法更漂亮的句子,我们想在除了最后一对数据之外的全部数据对之间插入一个逗号, 最后一对数据之间我们放一个单词「or」。【译注:当然这符合的是英文语法。 中文可能更常见的是顿号和「和」】「直到 -1元素的切片」 ([:-1]) 给出了除最后数据之外的全部数据,它们之间用「逗号加空格」分隔合并。

当然,这个代码在边际条件下无法运行,当 list 的长度为 0 或 1 时。

输出结果:

Choose red, blue, green or yellow

构建字符串, 变化 2

如果你需要应用一个函数来产生子字符串:

result = ''.join(fn(i) for i in items)

这涉及生成表达式(generator expression) ,我们随后会讨论。 如果你要增量计算子字符串的话,先将它们累积到一个列表中:

items = []
...
items.append(item)  # many times
...
# items is now complete
result = ''.join(fn(i) for i in items)

我们先把需要处理的部分累积到一个列表中, 然后我们就可以应用字符串方法 join ,高效处理。

可能的话就用 in (1)

Good:

for key in d:
    print key
  • in 通常情况下都会比较快。
  • 该模式也可以用于随机存取容器(比如 lists、tuples 和 sets)中的项
  • in 也是一个操作符 (我们马上就会看到).

Bad:

    for key in d.keys():
        print key

这会限制为只能操作有 keys() 方法的对象。

可能的话就用 in (2)

但当我们修改一个字典时, .keys() 是必需的:

for key in d.keys():
    d[str(key)] = d[key]

d.keys() 创建了一个包含字典键值的静态链表。 如果不这样做, 你会得到一个异常 "RuntimeError: dictionary changed size during iteration". 从一致性来考虑, 使用 key in dict, 而不是 dict.has_key():

# do this:
if key in d:
    ...do something with d[key]
# not this:
if d.has_key(key):
    ...do something with d[key]

在这种用法里, in 是一个操作符.

字典的 get 方法

We often have to initialize dictionary entries before use: 通常我们需要在使用之前初始化一个字典。

这是比较幼稚的做法:

navs = {}
for (portfolio, equity, position) in data:
if portfolio not in navs:
    navs[portfolio] = 0
navs[portfolio] += position * prices[equity]

dict.get(key, default) 移除了对条件判断的需要:

navs = {}
for (portfolio, equity, position) in data:
    navs[portfolio] = (navs.get(portfolio, 0)
                      + position * prices[equity])

直观多了。

字典的 setdefault 方法 (1)

现在我们要用初始化一个可变的字典的值。每个字典值是一个 list。 这是比较幼稚的做法:

Initializing mutable dictionary values:

equities = {}
for (portfolio, equity) in data:
    if portfolio in equities:
        equities[portfolio].append(equity)
    else:
        equities[portfolio] = [equity]

dict.setdefault(key, default) 可以以高得多的效率完成同样的工作:

equities = {}
for (portfolio, equity) in data:
    equities.setdefault(portfolio, []).append(equity)

dict.setdefault() 等价于 get, or set & get. 也就是 "如果有必要的话,set,然后 get. 它对于字典键值计算代价昂贵或很长难以输入的情况特别有价值。

dict.setdefault() 的唯一问题是默认值总是会被计算,无论需要与否。 这会在默认值计算代价昂贵的时候带来一些麻烦。

如果默认值确实计算代价昂贵,你可能希望使用 defaultdict 类, 我们下面很快就会涉及。

字典的 setdefault 方法 (2)

现在我们来看看字典方法 setdefault 也可以用在独立语句中。

setdefault 同样可以用于独立语句中:

navs = {}
for (portfolio, equity, position) in data:
    navs.setdefault(portfolio, 0)
    navs[portfolio] += position * prices[equity]

The setdefault dictionary method returns the default value, but we ignore it here. We're taking advantage of setdefault's side effect, that it sets the dictionary value only if there is no value already. 字典方法 setdefault 返回一个默认值,但是在这里我们忽略了它。 我们用到了 setdefault 的边际效应带来的优点, 它仅仅在字典对应键值没有值的情况下才会去设置值。

defaultdict

python 2.5 新增.

defaultdict 在 python 2.5中新增, 是 collections 模块中的一员. defaultdict 跟一个普通的字典完全一样, 除了两点:

  • 它在构造时带一个额外的第一参数: 一个默认工厂函数。并且
  • 当字典键值第一次被操作时,默认工厂函数被调用,产生的结果用于初始化该键值。

有两种方法可以得到一个 defaultdict:

导入 collections 模块并通过模块引用

import collections
d = collections.defaultdict(...)

或直接导入 defaultdict 名称:

from collections import defaultdict
d = defaultdict(...)

这是前面用过的一个例子,每一个字典值都必须被初始化成一个空 list, 现在用 defaultdict 改写它:

from collections import defaultdict
å
equities = defaultdict(list)
for (portfolio, equity) in data:
    equities[portfolio].append(equity)

根本没有做什么探索工作。在这个例子里,默认工厂函数是 list , 返回一个空 list。

这个例子演示了如何得到一个初始值为 0 的字典:使用 int 作为默认工厂函数:

navs = defaultdict(int)
for (portfolio, equity, position) in data:
    navs[portfolio] += position * prices[equity]

不过你还是得小心使用 defaultdict 。从一个正确初始化的 defaultdict 实例里你永远得不到 KeyError 异常。如果你需要检查特定键值存在与否, 必须使用「key in dict」条件判断。

构建和拆分字典

下面是一个很有用的技巧,如何通过两个 list (或其它序列)构建一个字典: 一个 list 是键,一个 list 是值。

given = ['John', 'Eric', 'Terry', 'Michael']
family = ['Cleese', 'Idle', 'Gilliam', 'Palin']
 pythons = dict(zip(given, family))
>>> pprint.pprint( pythons)
{'John': 'Cleese',
 'Michael': 'Palin',
 'Eric': 'Idle',
 'Terry': 'Gilliam'}

反过来,当然,也很直观:

>>>  pythons.keys()
['John', 'Michael', 'Eric', 'Terry']
>>>  pythons.values()
['Cleese', 'Palin', 'Idle', 'Gilliam']

注意结果中 .keys() 和 .values() 的顺序和构建字典时的项顺序不同。 进去的顺序跟出来的顺序不一样。这是因为字典本质上是非顺序的。然而, 这个顺序可以保证一致性(换句话说,键的顺序和值的顺序是一一对应的), 只要两次调用之间字典没有发生改变.

真值测试

# 这样做:         # 不要这样做:
if x:            if x == True:
    pass             pass

这种方法优雅且高效的利用了 python 对象内置的真值。

测试一个列表:

# 这样做:         # 不要这样做:
if items:        if len(items) != 0:
    pass             pass

                 # 千万不要这样做:
                 if item
                     pass

真值

TrueFalse 是两个 bool 类型的内置实例,布尔值。 跟 None 一样,它们各只会有一个实例存在。

| False | True | | ------| ------ | ------ | | False (== 0) | True (== 1) | | "" (空字符串) 除了 "" 之外的所有字符串(" ", "anything") | | 0, 0.0 | 除了 0 之外的所有数(1, 0.1, -1, 3.14) | | [], (), {}, set() | 任何非空容器 ([0], (None,), ['']) | |None` | 几乎所有未被显式置为False 的对象。 |

对象真值示例:

>>> class C:
...  pass
...
>>> o = C()
>>> bool(o)
True
>>> bool(C)
True

(Examples: execute truth.py.)

想要控制一个用户定义类实例的真值,使用特殊方法 __nonzero____len__ 。如果你的类是一个有长度的容器:

class MyContainer(object):

    def __init__(self, data):
        self.data = data

    def __len__(self):
            """Return my length."""
        return len(self.data)

如果你的类不是一个容器,使用 __nonzero__:

class MyClass(object):

    def __init__(self, value):
        self.value = value

    def __nonzero__(self):
        """Return my truth value (True or False)."""
        # This could be arbitrarily complex:
        return bool(self.value)

在 Python 3.0 中,为了与 bool 内置类型保持一致性, __nonzero__ 被改名成了 __bool__ 。为了保持兼容性,在类中增加如下定义:

__bool__ = __nonzero__

索引和项 (1)

当你需要一个单词列表的时候,有种可爱的方法可以节省你的打字时间:

>>> items = 'zero one two three'.split()
>>> print items
['zero', 'one', 'two', 'three']

假设我们需要遍历这些项,并且我们同时需要项和项的索引值:

                      -  -
i = 0
for item in items:              for i in range(len(items)):
    print i, item                   print i, items[i]
    i += 1

索引和项 (2): enumerate

函数 enumerate 接受一个 list,并返回 (索引值, 项) 对:

>>> print list(enumerate(items))
[(0, 'zero'), (1, 'one'), (2, 'two'), (3, 'three')]

输出结果时,我们得用一个 list 进行一下包装,因为 enumerate 是一个惰性函数: 它仅仅在需要时才一次产生一项,一个索引项对。 for 循环就是一个一次需要一个结果的所在。 enumerate 是生成器(generator) 的一个例子,我们将会在以后详细的讲解生成器。 print 不是一次拿一个结果 —— 我们需要整个结果,所以我们在打印时, 必须显式的将一个生成器转换为一个 list。

我们的循环变得简单多了:

for (index, item) in enumerate(items):
    print index, item

# 对比:                     # 对比:
index = 0                  for i in range(len(items)):
    for item in items:         print i, items[i]
    print index, item
    index += 1

enumerate 版本比左边的版本短得多也简单得多,而且也更加易读易理解。

下面这个例子展现 enumerate 函数实际上是如何返回一个迭代器(iterator)的 (生成器就是一种迭代器):

>>> enumerate(items)
<enumerate object at 0x011EA1C0>
>>> e = enumerate(items)
>>> e.next()
(0, 'zero')
>>> e.next()
(1, 'one')
>>> e.next()
(2, 'two')
>>> e.next()
(3, 'three')
>>> e.next()
Traceback (most recent call last):
    File "<stdin>", line 1, in ?
StopIteration

其它语言拥有「变量」

在很多其它语言中,对一个变量赋值,就是将一个值放入一个盒子中。 int a = 1; a1box.png

盒子「a」现在存放了一个整数1。

将其它值赋值给同一个变量就是替换盒子中的内容: a = 2; a2box.png

现在盒子「a」存放了整数2。

将一个变量赋值给其它变量,就是复制一个值,并将它放入新的盒子: int b = a; b2box.png a2box.png

「b」是第二个盒子,存放着一个经过复制的2。盒子「a」里有一份独立的备份。

Python拥有「名称」

在 python中,「名字」或「标识符」就像一个打在对象上的邮件标签(也就是一个命名标签)。 a = 1 a1tag.png

在这里,整数1对象有一个标签,上面写着「a」。

如果我们对「a」重新赋值,我们只是将这个标签移动到了其它对象:

a = 2 a2tag.png 1.png

现在名字「a」跟整数2对象联系在了一起。

原先的整数1对象不再有标签「a」了。对象本身可能还存在, 但是我们不能通过名字「a」得到它了。(当一个对象身上没有任何标签或者引用, 它会被从内存中删除。)

如果我们将一个名字赋值给另一个,我们只是为一个已经存在的对象挂上另一个名字的标签:

b = a ab2tag.png

名字「b」只是挂到原先「a」所指对象的第二个标签。

虽然我们即使在 python中通常也使用「变量」(因为这是一个常用术语), 我们实际上指的是「名字」或「标识符」。在 python 里, 「变量」是值的命名标签,而不是一个写了名字的盒子。

如果你没有从本教程中得到别的什么收获, 我希望你至少能理解 python「名字」是如何工作的。 这种理解一定会成为额外的红利,帮助你避免以下情形: ➔

参数默认值

这是一个初学者常见的错误。甚至一些有经验的程序员, 如果他们没有理解 python「名字」,也会犯同样的错误。

def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

问题在于 a_list 的默认值,一个空列表,是在函数定义时被求值的。 所以每次调用这个函数,我们得到的是同一个默认值。试试重复做几次:

>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

列表是一个可变对象;我们可以改变它们的内容。 得到一个默认列表的正确方法是在运行时创建,在函数内部:

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

% 字符串格式化

python的 % 操作符很像 C 语言的 sprintf 函数那样运作。 就算你对 C 语言不了解,关系也不大。基本上,你给出了一个模板, 或者说是格式,以及用于插入的数据。

在这个例子里,模板包含了两个转换规则:「%s」表示「在这里插入一个字符串」, 「%i」表示「将一个整型数转换成一个字符串并插入到这里」。 「%s」特别有用,因为它使用了 Python 的内置 str() 函数将任何对象转换成字符串。

用于插入的数据必须跟模板匹配;这里我们用了两个数据,一个 tuple。

name = 'David'
messages = 3
text = ('Hello %s, you have %i messages'
        % (name, messages))
print text

输出结果:

Hello David, you have 3 messages

详细的讨论在 python 库参考 ,第 2.3.6.2 节, 「字符串格式化操作」 。 一定要记得去看! 如果你还没做,去 Python.org ,下载 HTML 格式的文档(zip 包或者 tar 包), 装到自己的电脑上。没什么事比最终文档就在你手边更好的。

高级 % 字符串格式化

大多数人都不知道有另外一种更灵活的字符串格式化方式: 利用字典键值:

values = {'name': name, 'messages': messages}
print ('Hello %(name)s, you have %(messages)i '
       'messages' % values)

这里我们指定了插入数据的名字,这些名字可以在随后提供的字典中找到。

注意到什么冗余之处没有? 名字 「name」 和 「messages」已经在本地命名空间中定义了。 我们可以利用这一点。

使用本地命名空间中的名字:

print ('Hello %(name)s, you have %(messages)i '
       'messages' % locals())

locals() 函数返回一个本地可用的名字的字典。

这非常强大。利用它,我们可以完成你想要的任何字符串格式化, 不用考虑模板的插入数据问题。

不过能力也会带来危险。(「能力越大,责任越大」) 如果对一个外部提供的模板字符串使用 locals() 形式, 你实际上将你的整个本地命名空间都暴露给了调用者。你必须对此保持警惕。

检查你的本地命名空间:

>>> from pprint import pprint
>>> pprint(locals())

pprint 是一个非常有用的模块。如果你还不知道,尝试去把玩把玩。 它会让你调试数据结构更加轻松!

高级 % 字符串格式化

一个对象实例属性的名字空间实际上就是一个字典, self.__dict__ 。 使用实例名字空间中的名字:

print ("We found %(error_count)d errors"
       % self.__dict__)

等价于,但比下面的形式更灵活:

print ("We found %d errors"
       % self.error_count)

注意:类属性在类的 __dict__ 里。名字空间查找实际上是链式字典查找。

列表推导

列表推导 (List Comprehensions) (简写为「listcomps」) 是以下这种常见模式的语法简写: 传统方法,使用 for 和 if 语句:

new_list = []
for item in a_list:
    if condition(item):
        new_list.append(fn(item))

使用列表推导:

new_list = [fn(item) for item in a_list
    if condition(item)]

从某种程度上来说,列表推导清晰、简洁。你可以在列表推导中使用多个 for 循环和 if 条件,但是如果总数超过两个或三个, 或者条件非常复杂,我还是建议使用传统的 for 循环。套用 python之禅 的说法,选择更易读的那个。 举个例子来说,0-9 的平方列表:

>>> [n ** 2 for n in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

0-9 中奇数的平方列表:

>>> [n ** 2 for n in range(10) if n % 2]
[1, 9, 25, 49, 81]

生成器表达式(1)

假设我们要求 1-100 的平方的和: 循环解法:

total = 0
for num in range(1, 101):
    total += num * num

通过构造适当的序列,我们可以使用 sum 函数更快的达到目的。

列表推导解法:

total = sum([num * num for num in range(1, 101)])

生成器表达式解法:

total = sum(num * num for num in xrange(1, 101))

生成器表达式 (Generator expressions) (「genexps」) 跟列表推导很像, 不过列表推导是贪婪的,而生成器表达式是惰性的。列表推导一次性计算出 整个列表的全部结果,并以列表形式返回。生成器表达式只会在需要时一次计算一个数据, 以一个单独的值形式返回。这对于那种对很长的序列求解, 但计算结果只是中间步骤而并非最终结果的情况特别有用。

在上面的例子中,我们只关心求和的结果,我们不需要列表中每个元素平方的中间结果。 基于同样的理由,我们使用了 xrange :它惰性产生结果,每次一个。

生成器表达式 (2)

举个例子来说,如果我们要计算数十亿整数的平方和,如果使用列表推导, 会内存溢出,但生成器表达式没有问题。不过要花些时间。

total = sum(num * num
    for num in xrange(1, 1000000000))

语法方面的差别在于列表推导有方括号,生成器表达式没有。 有些时候生成器表达式需要用括号括住,所以你最好每次都这样做。 规则摘要:

  • 当计算结果列表是最终结果时,使用列表推导。
  • 当计算结果列表仅作为中间步骤时,使用生成器表达式

这是我在最近工作中见到的一个例子。

我们需要为未来的合同产生一份月份数(同时以字符串形式和整型存在)到月份代码映射的字典。 这可以用一个逻辑行完成。

完成方法如下:

  • 内置的 dict() 函数产生一个键/值对列表 (2个 tuple).
  • 我们有一份月份代码列表(每个月份代码是一个单字母,字符串就是字母的列表)。 我们遍历这个列表获得月份代码及其索引值。
  • 月份数从 1开始,但 Python 索引值从 0 开始,所以月份数等于索引值 +1。
  • 我们希望同时以字符串和整型检查月份。所以我们使用 int() 和 str() 函数来做,并在此基础上进行查找。

最近的例子:

month_codes = dict((fn(i+1), code)
    for i, code in enumerate('FGHJKMNQUVXZ')
    for fn in (int, str))

month_codes 的结果:

{ 1:  'F',  2:  'G',  3:  'H',  4:  'J', ...
'1': 'F', '2': 'G', '3': 'H', '4': 'J', ...}

排序

在 Python 里对一个 list 进行排序非常简单:

a_list.sort()

(注意,list的排序是就地进行的:原先的 list 直接变成排序好的,而且 sort 方法不会返回这个list或其拷贝。) 但是当你不想以正常顺序 (比如,按第一列排序,然后按第二列,等等) 排序列表数据是应该怎么做呢?比如你想按先第二列排序,然后按第四列。

我们可以为内置的 sort 方法传入一个自定义函数:

    def custom_cmp(item1, item2):
        return cmp((item1[1], item1[3]),
                   (item2[1], item2[3]))

    a_list.sort(custom_cmp)

这行得通,不过在处理大型列表时会非常非常慢。

使用 DSU 进行排序 *

DSU = Decorate-Sort-Undecorate (修饰-排序-反修饰)

  • 注意: DSU现在一般用不到了。新的方法见下一节, 按键值排序

我们创建一个会自然排序的辅助列表,而不是创建一个自定义比较函数

# 修饰
to_sort = [(item[1], item[3], item)
    for item in a_list

# 排序
to_sort.sort()

# 反修饰:
a_list = [item[-1] for item in to_sort]

第一行我们创建一个包含 tuples 的列表,按优先级顺序复制排序列,后面跟着完整的数据记录 第二行做通常的 Python 排序,非常快速和高效。 第三行我们去获取排好序的列表的最后一个数据。记住,最后一个数据就是完整的数据记录。 我们扔掉排序列,它们已经完成了它们的工作,现在没用了。 这是一个空间、复杂度和时间的权衡。更简单、更快,但是我们需要复制一份原始列表。

按键值排序

Python 2.4 为 sort 列表方法引入了一个可选参数,「key」, 用于指定一个单参数函数来返回列表中每个元素用于比较的键值。举例来说:

    def my_key(item):
        return (item[1], item[3])
    to_sort.sort(key=my_key)

函数 my_key 会在操作 to_sort 列表每一个项时都调用一次。

You can make your own key function, or use any existing one-argument function if applicable: 你可以实现自己的key函数,或者在可能的情况下使用已经存在的单参数函数:

  • str.lower 可以用于按忽略大小写的字母顺序排序。
  • len 可以用于按项的长度排序 (字符串或容器)。
  • intfloat 可以用于对那些「数字型字符串」,比如 "2", "123", "35" 之类, 按实际数字大小排序。

生成器

我们已经见过生成器表达式了。我们还可以定制我们自己的任意复杂的生成器, 就像定义函数一样:

    def my_range_generator(stop):
        value = 0
        while value < stop:
            yield value
            value += 1

    for i in my_range_generator(10):
        do_something(i)

yield 关键字把一个函数转变成一个生成器 (generator)。当我们调用一个生成器函数, Python 并不是立即去运行代码,而是返回一个生成器对象,本质上它是一个迭代器; 它有一个 next 方法。 for 循环每次都会去调用迭代器的 next 方法, 直到 StopIteration 异常抛出。 你也可以显式抛出 StopIteration , 或者如上所示,在生成器代码运行结束时隐式抛出。

生成器可以简化序列/迭代器的处理,因为我们不需要去构建一个具体的列表; 只需要一次计算一个值。 生成器函数需要维护状态。

【译注:说到「需要维护状态」,我们必须首先搞清楚函数的「状态」。一般的函数,只要不使用全局变量,我们都认为它是「无状态的」,也就是说,任何时候,我们调用函数,传入相同的输入参数, 必能得到相同的输出结果。但生成器函数不是这样,它是「带状态的」,我们使用相同输入参数去调用, 每次会得到不同的结果。其结果取决于上一次调用的「状态」。因此它是带状态的,我们需要仔细维护这种状态,否则我们会得不到想要的结果】

这就是 for 循环的实际工作原理。Python 在 in 关键字后面寻找一个序列。如果它是一个简单的容器 (比如list、tuple、dictionaryset,或者用户定义的容器), Python 会把容器转换成一个迭代器。 如果它已经是一个迭代器, Python 就直接使用它。

Python 重复调用迭代器的 next 方法,将返回值复制给循环计数器(本例中就是 i ), 然后执行循环体内的代码。这个动作周而复始,直到抛出 StopIteration 异常, 或者在代码中执行到 break 语句。

for 循环可以有一个 else 子句,它会在迭代器运行完成后执行, 但不会在 break 语句之后执行。这种区别可以用来完成一些优雅的使用。 else 子句在 for 循环中并不常用,但它们可以派上用场。 有时 else 子句可以完美的表达你需要的逻辑。

举个例子,如果我们需要对一个序列中的项做条件检查,只要任意项的条件符合就通过:

    for item in sequence:
        if condition(item):
            break
    else:
        raise Exception('Condition not satisfied.')

生成器举例

从一个 CSV reader (或一个 list 的项)中过滤空行:

def filter_rows(row_iterator):
    for row in row_iterator:
        if row:
            yield row

data_file = open(path, 'rb')
irows = filter_rows(csv.reader(data_file))

从文本/数据文件中按行读取

datafile = open('datafile')
for line in datafile:
    do_something(line)

这样做是可以的,因为文件对象支持 next 方法,就像其它迭代器所做的那样: lists、tuples、字典(的键值)、生成器。

这里有一个警告: 因为缓存实现方法的区别,你不能混合使用 .next.read* 方法, 除非你用的是 Python 2.5+。

EAFP vs. LBYL

It's easier to ask forgiveness than permission EAFP: 获得宽恕比获得许可要容易

Look before you leap LBYL: 三思而后行

一般来说 EAFP 更好,但不是任何时候。

  • 鸭子类型 如果一个东西走起来像鸭子,叫起来像鸭子,看起来像鸭子:它就是鸭子。 (鹅?反正看起来差不多。)
  • 异常 如果一个对象必须是一个特定类型,使用强制转换。 如果 x 必须是一个字符串才能让代码工作,为什么不直接调用
str(x)

而不是去做尝试:

isinstance(x, str)

EAFP风格的 try/except 举例

你可以把容易发生异常的代码包裹进一个 try/except 代码块来捕获错误, 你可以最终会给出一个非常通用的解决方案,而不是试图预测每一种可能性。

try:
    return str(x)
except TypeError:
    ...

注意:一定要指定捕获的异常。永远不要使用裸 except 子句【译注:不带任何异常声明的 except, 如: except: pass 这样】。裸 except 子句会捕获你不希望捕获的异常, 让你的代码非常难以调试。

导入

from module import *

你可能见过这种「通配符」形式的导入语句。你甚至可能喜欢这样做。 不要这样用。

【译注:以下一段是某个 Python 幽默段子的片段,背景是《星球大战》。 为了快速翻译及我认为不会太影响理解,我就不费心去查找和介绍背景资料了。 感兴趣的同学请自行去查看原文和相关背景。】

To adapt a well-known exchange:

(Exterior Dagobah, jungle, swamp, and mist.) 卢克: from module import * 比显式 imports 要好吗? 犹达: 不,不是更好。更快、更容易,更妖媚。 卢克: 但是我如何知道显式的 imports 比通配符形式的更好? 犹达:从现在开始的六个月后你试图读自己的代码时就会知道。

通配符导入来自 Python 的黑暗面。

永远不要!

from module import * 通配符形式会导致命名空间污染。 你会在你的本地命名空间里得到你根本得到的东西。 你也许会发现导入的名字模糊了本地模块定义的名字。你可能无法辨认一个 具体的名字到底来自哪里。也许这样做方便快捷,但它不该存在于生产代码之中。

正义的呼声: 不要使用通配符导入!

更好的做法:

  • 通过模块引用名字(完整规格的标识符)
  • 使用一个较短的名称导入一个较长名字的模块 (别名; 推荐),
  • 或者显式的仅仅导入你要用的名字。

命名空间污染警告! 相反地, 通过模块引用名字(完整规格的标识符):

import module
module.name

或者通过较短的名称导入较长名字的模块 (别名):

import long_module_name as mod
mod.name

或者显式的仅仅导入你需要的名字:

from module import name
name

注意交互环境下,当你需要编辑和「重载 (reload())」一个模块时,这种形式 不适合用于自身。

模块和脚本

想制作一个既是可导入的模块又是可执行的脚本:

if __name__ == '__main__':
    # script code here

当模块被导入时,它的 __name__ 属性被设置成模块的文件名,不含「.py」。 因此上面那段用 if 语句守护着的代码不会在导入时被执行。 当以脚本形式执行的时候, __name__属性会被设置称 "__main__", 这段脚本代码 会 运行。

除了一些特殊情况,你不应该在最顶层放置任何重要可执行代码。 将代码放入函数、类、方法,或者用 if __name__ == '__main__' 守护起来。

模块结构

"""module docstring(模块 docstring)"""

# imports(导入)
# constants(常量)
# exception classes(异常类)
# interface functions(接口函数)
# classes(类)
# internal functions & classes(内部函数和类)

def main(...):
    ...

if __name__ == '__main__':
    status = main()
    sys.exit(status)

模块应该拥有如上一样的代码结构.

命令行处理

Example: cmdline.py:

#!/usr/bin/env  python
"""
Module docstring.
"""

import sys
import optpars

def process_command_line(argv):
    """
    Return a 2-tuple: (settings object, args list).
    `argv` is a list of arguments, or `None` for ``sys.argv[1:]``.
    """
    if argv is None:
        argv = sys.argv[1:]

    # initialize the parser object:
    parser = optparse.OptionParser(
        formatter=optparse.TitledHelpFormatter(width=78),
        add_help_option=None)

    # define options here:
    parser.add_option(      # customized description; put --help last
        '-h', '--help', action='help',
        help='Show this help message and exit.')

    settings, args = parser.parse_args(argv)

    # check number of arguments, verify values, etc.:
    if args:
        parser.error('program takes no command-line arguments; '
                '"%s" ignored.' % (args,))

    # further process settings & args if necessary

    return settings, args

def main(argv=None):
    settings, args = process_command_line(argv)
    # application code here, like:
    # run(settings, args)
    return 0        # success

if __name__ == '__main__':
    status = main()
    sys.exit(status)

package/
    __init__.py
    module1.py
    subpackage/
        __init__.py
        module2.py
  • 用来组织你的项目.
  • 减少加载路径的数量.
  • 减少导入名字冲突.

Example:

import package.module1
from package.subpackage import module2
from package.subpackage.module2 import name

在 Python 2.5中我们可以通过一个future import来实现绝对导入和相对导入:

from __future__ import absolute_import

我自己还没有深入研究过这个特性,所以我们不会深入讨论它。

简单好过复杂

调试的难度是你第一次写代码的难度的两倍。因此,如果你写了尽你可能聪明的代码, 你,从定义上说,没聪明到可以调试它。—Brian W. Kernighan, co-author of The C Programming Language and the "K" in "AWK"

换句话说,保持你的程序简单!

不要重新发明轮子

在着手写任何代码之前,

  • 检查 Python 标准库。
  • 检查 Python 包索引(pypi) (乳酪店): 【译注,现在的 pypi 已经升级了,不再是乳酪店了,直接访问 http://pypi. python.org/ 即可 】

    http://cheeseshop. python.org/pypi

  • 搜索网络. Google 是你的朋友.

参考文献

tags: reformat


BMC logoBuy me a coffee via Alipay or Wechat Pay