原文:http://inventwithpython.com/beyond/chapter6.html
强大对于编程语言来说是一个没有意义的形容词。每种编程语言都称自己长处。官方 Python 教程开头就说 Python 是一种简单易学、功能强大的编程语言。但是没有一种语言可以做另一种语言不能做的算法,也没有量化编程语言“能力”的度量单位(尽管你可以用编程需要在程序员中受欢迎的成都来度量)。
但是每种语言都有自己的设计模式和缺陷,展现了它的优点和缺点。要像真正编写 Python 风格的 Python 代码,你需要知道的不仅仅是语法和标准库,进一步还学习它的习惯用法,或者专门的 Python 的编码实践。某些 Python 语言的特性有助于您编写 Python 风格的代码。
在这一章中,我将提供几种编写地道 Python 代码的常用方法以及相应案例。不同的程序员认为 Python 风格是不同的,但是它通常包括我在这里讨论的例子和实践。有经验的 Python 程序员使用这些技术,所以熟悉它们可以让您在现实世界的代码中识别它们。
Python 之禅
Tim Peters 的《Python 之禅》是一套 20 条关于 Python 语言和 Python 程序设计的指南。您的 Python 代码不一定要遵循这些指导方针,但是记住它们是有好处的。Python 之禅也是一个复活节彩蛋,或者隐藏的笑话,当你运行import this
时出现:
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
`--snip--`
注
不可思议的是,只有 19 条指导方针被写了下来。据报道,Python 的创始人吉多·范·罗苏姆说,丢失的第 20 句格言是“蒂姆·彼得斯开的一个奇怪的玩笑”,他让 Gudio 去填补空白,而他似乎从来没有抽出时间去做这件事。
最后,这些指导方针是大多数程序员支持或反对的观点。像所有好的道德准则一样,它们看似矛盾,但却提供最大的灵活性。以下是我对这些格言的解读:
- 漂亮总比丑陋好。漂亮的代码被认为是易读易懂的。程序员经常快速编写代码,而不考虑可读性。计算机可以运行不可读的代码,但不可读的代码对于人类程序员来说很难维护和调试。美丽是主观的,但是不考虑如何理解的代码对其他人来说通常是丑陋的。Python 受欢迎的原因是它的语法不像其他语言那样充斥着晦涩的标点符号,这使得它很容易使用。
- 显性比隐性好。如果我只写“这是不言自明的”,我会为这句格言提供一个糟糕的解释。同样,在代码中,最好是详细和明确的。您应该避免将代码的功能隐藏在晦涩难懂的语言特性后面,这些特性需要对语言有很深的了解才能完全理解。
- 简单胜于复杂。复杂总比蛮干好。这两句格言提醒我们,我们可以用简单或复杂的技术建造任何东西。如果你有一个需要铲子的简单问题,使用 50 吨的液压推土机就太大材小用了。但是对于一项巨大的工作来说,操作一台推土机的复杂性要比协调一个 100 人的团队的复杂性更可取。喜欢简单胜过复杂,但要知道简单的局限。
- 扁平比嵌套好。程序员喜欢将他们的代码组织成类别,尤其是包含子类别的类别,子类别包含其他子子类别。这些等级制度与其说增加了组织,不如说增加了官僚主义。只在一个顶级模块或数据结构中编写代码是可以的。如果你的代码看起来像
spam.eggs.bacon.ham()
或spam['eggs']['bacon']['ham']
,那么你的代码太复杂了。 - 稀不如密。程序员通常喜欢将尽可能多的功能塞进尽可能少的代码中,就像下面这样:
print('\n'.join("%i bytes = %i bits which has %i possiblevalues." % (j, j*8, 256**j-1) for j in (1 << i for i in range(8))))
。虽然像这样的代码可能会给他们的朋友留下深刻印象,但它会激怒他们的同事,他们不得不试图理解它。不要让你的代码一次做太多事情。分散在多行中的代码通常比密集的一行代码更容易阅读。这句格言大致和简单比复杂好一样。 - 可读性很重要。尽管对于从 20 世纪 70 年代就开始用 C 语言编程的人来说,
strcmp()
可能意味着“字符串比较”函数,但现代计算机有足够的内存来写出完整的函数名。不要从你的名字中去掉字母或者写过于简洁的代码。花点时间为变量和函数想出描述性的、具体的名字。代码各部分之间的空行可以起到与书籍中的段落分隔符相同的作用,让读者知道哪些部分应该一起阅读。这句格言大致和美丽胜于丑陋一样。 - 特例很少十,提倡以特殊违反规则。虽然实用性战胜了纯粹性。这两句格言互相矛盾。编程中充满了程序员应该在代码中努力实现的“最佳实践”。绕过这些实践进行快速破解可能很诱人,但可能会写出不一致、不可读的代码。另一方面,竭尽全力遵守规则会导致高度抽象、不可读的代码。例如,Java 编程语言试图让所有代码都符合其面向对象的范例,这通常会导致即使是最小的程序也有大量的样板代码。随着经验的积累,在这两个格言之间游走变得更加容易。假以时日,你就会明白“规则是用来打破的”这个道理。
- 错误永远不会悄无声息地过去。除非您选择沉默。仅仅因为程序员经常忽略错误信息并不意味着程序应该停止发出错误警告信息。当函数返回错误代码或
None
而不是引发异常时,可能会发生无声错误。这两句格言告诉我们,对于一个程序来说,让它快速失败和崩溃比选择漠视它要好。后来不可避免地发生的错误将更难调试,因为它们是在最初更容易被检测分析到。尽管你总是可以决定明确地忽略程序引起的错误,但是要心里明白你自己忽略了那些错误。 - 面对模棱两可的问题时,不要瞎猜。电脑让人类变得迷信:为了驱除电脑中的恶魔,我们会举行一个神圣的仪式,关掉电脑,然后再打开。据说这将解决任何神秘的问题。但是电脑不是魔术。如果你的代码不能工作,那是有原因的,只有仔细的、批判性的思考才能解决问题。拒绝盲目尝试解决方案的诱惑,直到事情似乎奏效;通常,你只是掩盖了问题,而不是解决了问题。
- 应该有一种——最好只有一种——显而易见的方法来做这件事。这是对 Perl 编程语言格言“有不止一种方法可以做到这一点”的观点是完全对立的,用三种或四种不同的方式来编写完成相同任务的代码是一把双刃剑:您可以灵活地编写代码,但现在您必须学习每种可能的方式来阅读其他人的代码。这种灵活性不值得花更多的精力去学习一门编程语言。
- 虽然除非你是荷兰人,否则一开始这种方式并不明显。这句话是个笑话。Python 之父吉多·范·罗苏姆是荷兰人。
- 现在总比没有好。尽管从来没有比现在更好。这两句格言告诉我们,运行慢的代码明显不如运行快的代码。但是等待你的程序完成,总比过早完成的错误程序要好。
- 如果实现很难解释,就尽量不要这样做。如果实现很容易解释,这可能是一个好主意。随着时间的推移,许多事情变得越来越复杂:税法、浪漫关系、Python 编程书籍。软件也不例外。这两句格言提醒我们,如果代码复杂到程序员无法理解和调试,那么它就是糟糕的代码。但是仅仅因为向别人解释一个程序的代码很容易,并不意味着它是好代码。不幸的是,弄清楚如何使代码尽可能简单,而不是更简单,这通常很难做到。
- 命名空间是一个非常棒的想法——让我们多做一些吧!命名空间是标识符的独立容器,以防止命名冲突。比如
open()
内置函数和webbrowser.open()
函数同名但引用不同的函数。导入webbrowser
不会覆盖内置的open()
函数,因为两个open()
函数存在于不同的名称空间中:分别是内置的名称空间和webbrowser
模块的名称空间。但是请记住,扁平的比嵌套的好:尽管名称空间很大,但是您应该只使用它们来防止命名冲突,而不是添加不必要的分类。
和所有关于编程的观点一样,你可以反驳我在这里列出的观点,或者你看后无感。争论应该如何编写代码或者什么才算“Python 风格化”意义并不大。(除非你正在写一本充满编程观点的书。)
学会使用缩进
我从来自其他语言的程序员那里听到的关于 Python 的最常见的担忧是,Python 的有效缩进(经常被误称为有效空格)是怪异和陌生的。在 Python 中,一行代码开头的缩进量是有意义的,因为它决定了哪些代码行在同一个代码块中。
使用缩进对 Python 代码块进行分组可能看起来很奇怪,因为其他语言用大括号{
和}
来声明代码块的开始和结束。但是非 Python 语言的程序员通常也缩进他们的块,就像 Python 程序员一样,以使他们的代码更可读。例如,Java 编程语言没有明显的缩进。Java 程序员不需要缩进代码块,但为了可读性,他们经常这样做。下面的例子有一个名为main()
的 Java 函数,它包含一个对println()
函数的调用:
// Java Example
public static void main(String[] args) {
System.out.println("Hello, world!");
}
如果println()
行没有缩进,这段 Java 代码仍然可以运行,因为括号是 Java 中标记代码块的开始和结束,而不是缩进。Python 不允许缩进是可选的,而是强制代码具有一致的可读性。但是注意 Python 没有有效空格这个概念,因为 Python 并没有限制你如何使用非有效空格(两个2 + 2
和2+2
都是有效的 Python 表达式)。
一些程序员认为左大括号应该和开始语句在同一行,而另一些人认为应该在下一行。程序员会争论他们喜欢的风格的优点,直到时间的尽头。Python 巧妙地避开了这个问题,根本不使用大括号,让 Python 编码者避开无意义的讨论, 回到更高效的工作中。我开始希望所有的编程语言都采用 Python 的方法对代码块进行分组。
但是有些人仍然渴望大括号,并希望将它们添加到 Python 的未来版本中——尽管这种想法是多么不合时宜。Python 的__future__
模块将特性反向移植到早期的 Python 版本,如果你试图将括号特性导入 Python,你会发现一个隐藏的复活节彩蛋:
>>> from __future__ import braces
SyntaxError: not a chance
我不指望大括号会很快被加入 Python。
经常被误用的语法
如果 Python 不是你的第一编程语言,你可以用和其他编程语言一样的策略来编写你的 Python 代码。或者,您可能学习了一种不寻常的编写 Python 代码的方法,因为您不知道有更多已确立的最佳实践。这段笨拙的代码可以工作,但是通过学习编写 Python 风格代码的更标准的方法,您可以节省一些时间和精力。本节解释了程序员常犯的错误,以及应该如何编写代码。
使用enumerate()
而不是range()
当循环遍历一个列表或其他序列时,一些程序员使用range()
和len()
函数来生成从0
到序列长度的索引整数,但不包括序列长度。在这些for
循环中使用变量名i
(用于索引)是很常见的。例如,在交互式 Shell 中输入以下非单调示例:
>>> animals = ['cat', 'dog', 'moose']
>>> for i in range(len(animals)):
... print(i, animals[i])
...
0 cat
1 dog
2 moose
约定很简单,但是不太理想,因为它可能很难阅读。相反,将列表或序列传递给内置的enumerate()
函数,该函数将返回索引和该索引处的项目的整数。例如,您可以编写以下 Python 风格代码:
>>> # Pythonic Example
>>> animals = ['cat', 'dog', 'moose']
>>> for i, animal in enumerate(animals):
... print(i, animal)
...
0 cat
1 dog
2 moose
用enumerate()
代替range(len())
,你写的代码会稍微干净一点。如果只需要条目而不需要索引,仍然可以用 Python 的方式直接遍历列表:
>>> # Pythonic Example
>>> animals = ['cat', 'dog', 'moose']
>>> for animal in animals:
... print(animal)
...
cat
dog
moose
调用enumerate()
并直接迭代一个序列比使用老式的range(len())
约定更好。
使用with
语句代替open()
和close()
函数
函数将返回一个包含读写文件方法的文件对象。完成后,file
对象的close()
方法会关闭该文件,使该文件可供其他程序读写。您可以单独使用这些函数。但是这样做是不严谨的。例如,在交互式 Shell 中输入以下内容来编写文本'Hello, world!'
到一个名为spam.txt
的文件:
>>> # Unpythonic Example
>>> fileObj = open('spam.txt', 'w')
>>> fileObj.write('Hello, world!')
13
>>> fileObj.close()
如果在try
块中发生错误,程序跳过对close()
的调用,以这种方式编写代码会导致文件不关闭。例如:
>>> # Unpythonic Example
>>> try:
... fileObj = open('spam.txt', 'w')
... eggs = 42 / 0 # A zero divide error happens here.
... fileObj.close() # This line never runs.
... except:
... print('Some error occurred.')
...
Some error occurred.
在到达零除错误时,执行移动到except
块,跳过close()
调用并保持文件打开。这可能导致文件受损错误,以后排错很难追溯到try
块。
相反,当执行离开with
语句的块时,可以使用with
语句自动调用close()
。下面的 Python 风格示例与本节中的第一个示例是等价的:
>>> # Pythonic Example
>>> with open('spam.txt', 'w') as fileObj:
... fileObj.write('Hello, world!')
...
即使没有对close()
的显式调用,当执行离开块时,with
语句也会自动调用它。
使用is
而不是==
与None
进行比较,
==
相等运算符比较两个对象的值,而is
相同运算符比较两个对象的标识。第 7 章涵盖了值和标识。两个对象可以存储相等的值,但是作为两个独立的对象意味着它们有独立的标识。然而,每当你比较一个值和None
时,你应该总是使用is
操作符而不是==
操作符。
在某些情况下,表达式spam == None
可以计算为True
,即使spam
只包含None
。这可能是由于==
操作符过载造成的,第 17 章对此有更详细的介绍。但是spam is None
会检查spam
变量中的值是否是字面上的None
。因为None
是NoneType
数据类型的唯一值,所以任何 Python 程序中都只有一个None
对象。如果变量被设置为None
,则is None
比较将总是求值为True
。第 17 章描述了重载==
操作符的细节,但下面是这种行为的一个例子:
>>> class SomeClass:
... def __eq__(self, other):
... if other is None:
... return True
...
>>> spam = SomeClass()
>>> spam == None
True
>>> spam is None
False
一个类以这种方式重载==
操作符的可能性很小,但是为了以防万一,总是使用is None
而不是== None
已经成为 Python 的习惯用法。
最后,你不应该使用带有值True
和False
的is
操作符。您可以使用==
相等运算符将一个值与True
或False
进行比较,例如spam == True
或spam == False
。更常见的是完全省略操作符和布尔值,编写类似于if spam:
或if not spam:
的代码,而不是if spam == True:
或if spam == False:
。
格式化字符串
字符串出现在几乎所有的计算机程序中,不管是哪种语言。这种数据类型很常见,所以有许多方法来操作和格式化字符串也就不足为奇了。本节重点介绍几个最佳案例。
如果您的字符串有许多反斜杠,请使用原始字符串
转义字符允许您将文本插入到字符串字面值中,否则将无法在文本中包含转移字符。例如,您需要'Zophie\'s chair'
中的\
,因此 Python 将第二个引号解释为字符串的一部分,而不是标记字符串结尾的符号。因为反斜杠有这个特殊的转义含义,如果你想在字符串中放一个实际的反斜杠字符,你必须输入它作为\\
。
原始字符串是带有前缀r
的字符串,它们不将反斜杠字符视为转义字符。相反,他们只是把反斜杠放到字符串中。例如,Windows 文件路径的这个字符串需要几个转义反斜杠,这不是很 Python 风格化:
>>> # Unpythonic Example
>>> print('The file is in C:\\Users\\Al\\Desktop\\Info\\Archive\\Spam')
The file is in C:\Users\Al\Desktop\Info\Archive\Spam
这个原始字符串(注意前缀r
)产生相同的字符串值,同时可读性更好:
>>> # Pythonic Example
>>> print(r'The file is in C:\Users\Al\Desktop\Info\Archive\Spam')
The file is in C:\Users\Al\Desktop\Info\Archive\Spam
原始字符串是一种特殊的字符串数据类型;它们只是一种输入包含几个反斜杠字符的字符串的便捷方式。我们经常使用原始字符串来键入正则表达式或 Windows 文件路径的字符串,这些字符串中经常有几个反斜杠字符,用\\
逐个转义会很麻烦。
用 F 字符串格式化字符串
字符串格式化,或字符串插值,是创建包含其他字符串的字符串的过程,在 Python 中有很长的历史。最初,+
操作符可以将字符串连接在一起,但这导致代码中有许多引号和加号:'Hello, ' + name + '. Today is ' + day + ' and it is ' + weather + '.'
。%s
转换说明符使语法变得简单了一点:'Hello, %s. Today is %s and it is %s.' % (name, day, weather)
。这两种技术都将把name
、day
和weather
变量中的字符串插入到字符串字面值中,以计算一个新的字符串值,就像这样:'Hello, Al. Today is Sunday and it is sunny.'
。
format()
字符串方法添加了格式规范迷你语言(docs.python.org/3/library/string.html#formatspec
),这涉及到以类似于%s
转换说明符的方式使用{}
大括号对。然而,这种方法有些复杂,会产生不可读的代码,所以我不鼓励使用它。
但是从 Python 3.6 开始, F 字符串(格式字符串的缩写)提供了一种更方便的方法来创建包含其他字符串的字符串。就像原始字符串在第一个引号前加前缀r
一样,F 字符串也加前缀f
。您可以在 F 字符串的大括号中包含变量名,以插入存储在这些变量中的字符串:
>>> name, day, weather = 'Al', 'Sunday', 'sunny'
>>> f'Hello, {name}. Today is {day} and it is {weather}.'
'Hello, Al. Today is Sunday and it is sunny.'
大括号也可以包含整个表达式:
>>> width, length = 10, 12
>>> f'A {width} by {length} room has an area of {width * length}.'
'A 10 by 12 room has an area of 120.'
如果需要在 F 字符串中使用大括号字面值,可以用一个额外的大括号进行转义:
>>> spam = 42
>>> f'This prints the value in spam: {spam}'
'This prints the value in spam: 42'
>>> f'This prints literal curly braces: {{spam}}'
'This prints literal curly braces: {spam}'
因为您可以将变量名和表达式内联到字符串中,所以您的代码比使用旧的字符串格式化方法更具可读性。
所有这些格式化字符串的不同方法都违背了 Python 的格言:应该有一种——最好只有一种——显而易见的方法来做某事。但是格式化函数是对语言的一种改进(在我的看来),正如另一条指导方针所说,实用性胜过纯粹性。如果只为 Python 3.6 或更高版本编写代码,请使用 F 格式化字符串。如果您正在编写早期 Python 版本运行的代码,请坚持使用format()
字符串方法或%s
转换说明符。
制作列表的浅层副本
切片语法可以很容易地从现有的字符串或列表中创建新的字符串或列表。在交互式 Shell 中输入以下内容,看看它是如何工作的:
>>> 'Hello, world!'[7:12] # Create a string from a larger string.
'world'
>>> 'Hello, world!'[:5] # Create a string from a larger string.
'Hello'
>>> ['cat', 'dog', 'rat', 'eel'][2:] # Create a list from a larger list.
['rat', 'eel']
冒号(:
)分隔要放入正在创建的新列表中的项目的开始和结束索引。如果省略冒号前的起始索引,如在'Hello, world!'[:5]
中,起始索引默认为0
。如果省略冒号后的结束索引,如['cat', 'dog', 'rat', 'eel'][2:]
,结束索引默认为列表的末尾。
如果省略两个索引,起始索引是0
(列表的开始),结束索引是列表的结尾。这实际上创建了列表的副本:
>>> spam = ['cat', 'dog', 'rat', 'eel']
>>> eggs = spam[:]
>>> eggs
['cat', 'dog', 'rat', 'eel']
>>> id(spam) == id(eggs)
False
请注意,spam
和eggs
中的列表标识是不同的。eggs = spam[:]
行创建了spam
中列表的浅副本,而eggs = spam
将只拷贝对列表的引用。但是[:]
看起来确实有点奇怪,使用copy
模块的copy()
函数来生成列表的浅层副本更具可读性:
>>> # Pythonic Example
>>> import copy
>>> spam = ['cat', 'dog', 'rat', 'eel']
>>> eggs = copy.copy(spam)
>>> id(spam) == id(eggs)
False
你应该知道这种少见的语法,以防你碰到使用它的 Python 代码,但是我不建议用你自己的代码写它。请记住,[:]
和copy.copy()
都可以创建浅层副本。
使用字典的 Python 风格方法
字典是许多 Python 程序的核心,因为键值对(将在第 7 章中进一步讨论)通过将一段数据映射到另一段数据提供了灵活性。因此,了解 Python 代码常用的一些字典习惯用法是很有用的。
关于字典的更多信息,请参考 Python 程序员 Brandon Rhodes 关于字典及其工作原理的精彩演讲:2010 年 PyCon 上的“强大的字典”,可在invpy.com/mightydictionary
观看;2017 年 PyCon 上的“更强大的字典”,可在invpy.com/dictionaryevenmightier
观看。
对字典使用get()
和setdefault()
试图访问一个不存在的字典键会导致一个KeyError
错误,所以程序员通常会编写啰嗦的代码来避免这种情况,就像这样:
>>> # Unpythonic Example
>>> numberOfPets = {'dogs': 2}
>>> if 'cats' in numberOfPets: # Check if 'cats' exists as a key.
... print('I have', numberOfPets['cats'], 'cats.')
... else:
... print('I have 0 cats.')
...
I have 0 cats.
这段代码检查字符串'cats'
是否作为关键字存在于numberOfPets
字典中。如果是,调用print()
访问numberOfPets['cats']
作为给用户的消息的一部分。如果没有,另一个print()
调用在没有访问numberOfPets['cats']
的情况下打印一个字符串,所以它不会引发KeyError
。
这种模式经常发生,以至于字典中有一个get()
方法,当字典中不存在某个键时,该方法允许您指定一个要返回的默认值。以下 Python 风格代码相当于前面的示例:
>>> # Pythonic Example
>>> numberOfPets = {'dogs': 2}
>>> print('I have', numberOfPets.get('cats', 0), 'cats.')
I have 0 cats.
调用numberOfPets.get('cats', 0)
检查关键字'cats'
是否存在于numberOfPets
字典中。如果是,方法调用返回'cats'
键的值。如果没有,它将返回第二个参数0
。使用get()
方法为不存在的键指定默认值比使用if-else
语句更短,可读性更好。
相反,如果一个键不存在,您可能希望设置一个默认值。例如,如果numberOfPets
中的字典没有'cats'
键,指令numberOfPets['cats'] += 10
将导致KeyError
错误。您可能希望添加代码来检查该键是否存在并设置默认值:
>>> # Unpythonic Example
>>> numberOfPets = {'dogs': 2}
>>> if 'cats' not in numberOfPets:
... numberOfPets['cats'] = 0
...
>>> numberOfPets['cats'] += 10
>>> numberOfPets['cats']
10
但是因为这种模式也很常见,所以字典有一个更 Python 风格化的setdefault()
方法。以下代码等效于前面的示例:
>>> # Pythonic Example
>>> numberOfPets = {'dogs': 2}
>>> numberOfPets.setdefault('cats', 0) # Does nothing if 'cats' exists.
0
>>> numberOfPets['cats'] += 10
>>> numberOfPets['cats']
10
如果您正在编写if
语句来检查字典中是否存在某个键,如果该键不存在,则设置默认值,请使用setdefault()
方法。
为默认值使用collections.defaultdict
您可以使用collections.defaultdict
类来完全消除KeyError
错误。这个类允许您通过导入collections
模块并调用collections.defaultdict()
来创建一个默认字典,向其传递一个数据类型以用作默认值。例如,通过将int
传递给collections.defaultdict()
,您可以创建一个类似字典的对象,它使用0
作为不存在的键的默认值。在交互式 Shell 中输入以下内容:
>>> import collections
>>> scores = collections.defaultdict(int)
>>> scores
defaultdict(<class 'int'>, {})
>>> scores['Al'] += 1 # No need to set a value for the 'Al' key first.
>>> scores
defaultdict(<class 'int'>, {'Al': 1})
>>> scores['Zophie'] # No need to set a value for the 'Zophie' key first.
0
>>> scores['Zophie'] += 40
>>> scores
defaultdict(<class 'int'>, {'Al': 1, 'Zophie': 40})
注意,您传递的是int()
函数,而不是调用它,所以您省略了collections.defaultdict(int)
中int
后面的括号。也可以通过list
使用空列表作为默认值。在交互式 Shell 中输入以下内容:
>>> import collections
>>> booksReadBy = collections.defaultdict(list)
>>> booksReadBy['Al'].append('Oryx and Crake')
>>> booksReadBy['Al'].append('American Gods')
>>> len(booksReadBy['Al'])
2
>>> len(booksReadBy['Zophie']) # The default value is an empty list.
0
如果您需要每个可能的键的默认值,使用collections.defaultdict()
比使用常规字典并不断调用setdefault()
方法要容易得多。
使用字典代替switch
语句
Java 之类的语言有一个switch
语句,这是一种if-elif-else
语句,它根据判定变量包含的众多值中的哪一个来运行代码。Python 没有switch
语句,所以 Python 程序员有时会编写类似下面这个例子的代码,根据season
变量包含的值运行不同的赋值语句:
# All of the following if and elif conditions have "season ==":
if season == 'Winter':
holiday = 'New Year\'s Day'
elif season == 'Spring':
holiday = 'May Day'
elif season == 'Summer':
holiday = 'Juneteenth'
elif season == 'Fall':
holiday = 'Halloween'
else:
holiday = 'Personal day off'
这段代码不一定不好看,但是有点冗长。默认情况下,Java switch
语句具有“跳转”功能,要求每个块以一个break
语句结束。否则,执行将继续到下一个块。忘记添加这个break
语句是一个常见的错误来源。但是在我们的 Python 例子中,所有的if-elif
语句都是重复的。一些 Python 程序员更喜欢设置一个字典值,而不是使用if-elif
语句。以下简洁的 Python 风格代码相当于前面的示例:
holiday = {'Winter': 'New Year\'s Day',
'Spring': 'May Day',
'Summer': 'Juneteenth',
'Fall': 'Halloween'}.get(season, 'Personal day off')
这段代码只是一条赋值语句。存储在holiday
中的值是get()
方法调用的返回值,它返回season
被设置的键的值。如果season
键不存在,get()
返回'Personal day off'
。使用字典会使代码更简洁,但也会使代码更难阅读。是否使用该特性由您决定。
条件表达式:Python 的“丑陋”三元运算符
三元运算符(正式名称为条件表达式,有时在 Python 中称为三元选择表达式)根据条件将表达式计算为两个值之一。通常,您会用 Python 风格的if-else
语句来实现这一点:
>>> # Pythonic Example
>>> condition = True
>>> if condition:
... message = 'Access granted'
... else:
... message = 'Access denied'
...
>>> message
'Access granted'
三元简单地表示一个有三个输入的操作符,但在编程中,它与条件表达式同义。条件表达式也为符合这种模式的代码提供了更简洁的一行程序。在 Python 中,它们是通过if
和else
关键字的奇怪排列来实现的:
>>> valueIfTrue = 'Access granted'
>>> valueIfFalse = 'Access denied'
>>> condition = True
>>> message = valueIfTrue if condition else valueIfFalse # 1
>>> message
'Access granted'
>>> print(valueIfTrue if condition else valueIfFalse) # 2
'Access granted'
>>> condition = False
>>> message = valueIfTrue if condition else valueIfFalse
>>> message
'Access denied'
如果condition
变量为True
,表达式valueIfTrue if condition else valueIfFalse
1 的计算结果为valueIfTrue
。当condition
变量为False
时,表达式计算结果为valueIfFalse
。吉多·范·罗苏姆开玩笑地将三元运算符是最丑的代码,然后是真值,最后是假值。您可以在任何可以使用表达式或值的地方使用条件表达式,包括作为函数调用 2 的参数。
为什么 Python 会在 Python2.5 中引入这种语法,尽管它打破了漂亮比难看好的第一条准则?不幸的是,尽管有些不可读,但许多程序员热衷于使用三元运算符,并希望 Python 支持这种语法。有可能滥用布尔运算符短路来创建一种三元运算符。如果condition
是True
,表达式condition and valueIfTrue or valueIfFalse
将计算为valueIfTrue
,如果condition
是False
,则计算为valueIfFalse
(除了一个重要的情况)。在交互式 Shell 中输入以下内容:
>>> # Unpythonic Example
>>> valueIfTrue = 'Access granted'
>>> valueIfFalse = 'Access denied'
>>> condition = True
>>> condition and valueIfTrue or valueIfFalse
'Access granted'
这种condition and valueIfTrue or valueIfFalse
风格的伪三进制运算符有一个微妙的 bug:如果valueIfTrue
是一个 false 值(如0
、False
、None
或空白字符串),那么如果condition
是True
,则表达式意外地计算为valueIfFalse
。
但是程序员还是继续使用这个假的三元运算符,还有“为什么 Python 没有三元运算符?”成为 Python 核心开发人员的一个长期问题。创建条件表达式是为了让程序员不再要求三元运算符,也不会使用容易出错的伪三元运算符。但是条件表达式也很丑陋,足以阻止程序员使用它们。虽然漂亮可能比难看好,但 Python 的“难看”三元运算符是实用性战胜纯粹性的一个例子。
条件表达式不完全是 Python 风格的,但也不是非 python 式的。如果您确实要使用它们,请避免将条件表达式嵌套在其他条件表达式中:
>>> # Unpythonic Example
>>> age = 30
>>> ageRange = 'child' if age < 13 else 'teenager' if age >= 13 and age < 18 else 'adult'
>>> ageRange
'adult'
嵌套的条件表达式是一个很好的例子,说明密集的一行程序在技术上是正确的,但在阅读时却令人沮丧。
使用变量值
您经常需要检查和修改变量存储的值。Python 有几种方法可以做到这一点。让我们看几个例子。
链接赋值和比较运算符
当您必须检查一个数字是否在某个范围内时,您可以像这样使用布尔and
运算符:
# Unpythonic Example
if 42 < spam and spam < 99:
但是 Python 允许你链接比较操作符,所以你不需要使用and
操作符。以下代码等效于前面的示例:
# Pythonic Example
if 42 < spam < 99:
这同样适用于链接=
赋值操作符。您可以在一行代码中将多个变量设置为相同的值:
>>> # Pythonic Example
>>> spam = eggs = bacon = 'string'
>>> print(spam, eggs, bacon)
string string string
要检查这三个变量是否都相同,可以使用and
操作符,或者更简单地说,将==
比较操作符链接起来以确保相等。
>>> # Pythonic Example
>>> spam = eggs = bacon = 'string'
>>> spam == eggs == bacon == 'string'
True
在 Python 中,链接操作符是一个小而有用的快捷方式。然而,如果你滥用它们,则会引起问题。第 8 章展示了一些使用它们会在你的代码中引入意想不到的错误的例子。
检查变量是否是许多值中的一个
有时,您可能会遇到与上一节中描述的情况相反的情况:检查单个变量是否是多个可能值中的一个。您可以使用or
操作符来做到这一点,比如在表达式spam == 'cat' or spam == 'dog' or spam == 'moose'
中。所有那些多余的“spam ==
”部分让这个表达有点笨拙。
相反,您可以将多个值放入一个元组中,并使用in
运算符检查该元组中是否存在变量值,如下例所示:
>>> # Pythonic Example
>>> spam = 'cat'
>>> spam in ('cat', 'dog', 'moose')
True
根据timeit
的说法,这个方式不仅更容易理解,而且速度也稍快。
总结
所有编程语言都有自己的习惯用法和最佳实践。本章重点介绍 Python 程序员编写“Python”代码的特殊方式,以充分利用 Python 的语法。
Python 代码的核心是来自 Python 禅宗的 20 条格言,它们是编写 Python 的粗略指南。这些格言只是观点,对于编写 Python 代码来说并不是绝对必要的,但是记住它们是有好处的。
Python 的显著缩进(不要与显著的空白混淆)引起了刚入坑 Python 程序员的最大抗议。虽然几乎所有的编程语言都使用缩进来使代码可读,但是 Python 将缩进作用提升了一个层次,编译器对缩进是有语义解释的。
尽管许多 Python 程序员对for
循环默认使用range(len())
,但是enumerate()
函数提供了一种更简洁的方法来获取索引和值,同时对序列进行迭代。同样,与手动调用open()
和close()
相比,with
语句是一种更干净、更不容易出错的文件处理方式。with
语句确保无论何时执行跳出with
语句块,都会调用close()
。
Python 有几种插入字符串的方法。最初的方法是使用%s
转换说明符来标记字符串应该包含在原始字符串中的位置。Python 3.6 的现代方式是使用 F 字符串。F 字符串以字母f
作为字符串的前缀,并使用大括号来标记可以在字符串中放置字符串(或整个表达式)的位置。
制作浅层列表副本的语法看起来有点奇怪,不一定是 Python 风格的,但它已经成为快速创建浅层列表的常用方法。
字典有一个get()
和setdefault()
方法来处理不存在的键。或者,collections.defaultdict
字典将对不存在的键使用默认值。另外,虽然 Python 中没有switch
语句,但是使用字典是一种简洁的方法来实现它的等价语句,而不需要使用几个if-elif-else
语句,并且在两个值之间求值时可以使用三元运算符。
一系列的==
操作符可以检查多个变量是否相等,而in
操作符可以检查一个变量是否是许多可能值中的一个。
本章讲述了几个 Python 语言习惯用法,为您提供了如何编写更多 Python 代码的提示。在下一章中,您将了解一些 Python 初学者容易掉入的陷阱。