[译]The Python Tutorial#More Control Flow Tools
除了刚才介绍的while
语句之外,Python也从其他语言借鉴了其他流程控制语句,并做了相应改变。
4.1 if
Statements
或许最广为人知的语句就是if
语句了。例如:
x = int(input("Please enter an integer: "))
if x < 0:
x = 0
print('Negative changed to zero')
elif x == 0:
print('Zero')
elif x == 1:
print('Single')
else:
print('More')
可以存在零个或者多个elif
子句,而且else
子句是可选的。关键字elif
是else if
的简写形式,可以避免过度的缩进。if ... elif ... elif ...
序列是其他语言中switch
或者case
语句的替代品。
4.2 for
Statements
Python中的for
语句与C或者Pacsal中的for
语句有微小差别。Python没有采用对等差数列数字迭代的方式(在Pascal中)实现循环,也没有采用如C语言一样赋予用户自定义迭代步数和停止条件的方式实现循环,Python的for
语句在任意序列(列表或者字符串)的项上迭代,以项在序列中的出现顺序迭代。例如(没有别的意思):
# Measure some strings:
words = ['cat', 'window', 'defenestrate']
for w in words:
print(w, len(w))
如果需要在循环内修改正在遍历的序列(例如复制选中项),推荐首先对序列复制。遍历序列时并没有隐式地创建备份。切片语句为复制序列提供了便利:
for w in words[:]: # Loop over a slice copy of the entire list.
if len(w) > 6:
words.insert(0, w)
words
使用for w in words:
,实例会尝试创建一个无限列表,不断地插入字符串defenestrate
。
4.3 The range()
Function
如果确实需要对数字的序列做迭代,built-in函数range()
可以派上用场。这个函数生成算术数列:
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
end
参数不会被包含到生成的序列中;range(10)
生成10个值,序列项的合法索引长度是10。可以让range在另一个数字上开始,或者指定一个不同的增量(甚至是负数;有时增量称作步长'step'):
range(5, 10)
5 through 9
range(0, 10, 3)
0, 3, 6, 9
range(-10, -100, -30)
-10, -40, -70
需要对序列的索引做迭代时,可以像下面这样结合range()
和len()
函数:
>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
... print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb
然而,在大多数这样的情况下,使用enumerate()
函数很方便,详细参考Looping Techniques。
直接打印range()的结果会很奇怪:
>>> print(range(10))
range(0, 10)
在许多情形中range()
返回的对象就像一个列表一样,但是事实上它并不是。当迭代这个对象时,它会返回目标序列中的连续项,但是为了节约空间,并没有真的创建一个列表。
这种对象被称为iterable
(可迭代的),即,若一些函数或者结构期待从某种东西中获得连续项,直到结束,那么iterable
对象可以满足这种需求。for
语句就是这样的迭代程序。list()
函数是另外一个,它使用可迭代对象创建列表:
>>> list(range(5))
[0, 1, 2, 3, 4]
接下来会介绍更多返回可迭代对象和使用可迭代对象作为参数的函数。
4.4 break
and continue
Statements, and else
Clauses on Loops
像C语言一样,break
语句跳出for
或者while
的最内层循环。
循环语句可以有else
子句;当for
循环遍历完循环序列或者while
循环的循环条件变为False
时,else
子句会执行。但是当循环被break
语句终止时,else
子句不会执行。以下寻找素数的实例可以证明这一点:
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, 'equals', x, '*', n//x)
... break
... else:
... # loop fell through without finding a factor
... print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
(代码是正确的。靠近看:else
子句属于for
循环,而不是if
语句。)
当与循环语句一起使用时,else
子句与try
语句中的else
子句相似,而与if
语句的else
子句不同:当没有异常发生时,try
语句的else
子句会执行,在循环中,没有break
发生时,else
子句会执行。想要了解更多关于try
语句和异常的信息,参见 Handling Exceptions。
continue
语句,也是从C语言借鉴而来,用于直接跳到循环的下一个迭代:
>>> for num in range(2, 10):
... if num % 2 == 0:
... print("Found an even number", num)
... continue
... print("Found a number", num)
Found an even number 2
Found a number 3
Found an even number 4
Found a number 5
Found an even number 6
Found a number 7
Found an even number 8
Found a number 9
4.5 pass
Statements
pass
语句什么也不做,当一个语句需要语法上的完整而不做任何事情时,可以使用pass
语句。例如:
>>> while True:
... pass # Busy-wait for keyboard interrupt (Ctrl+C)
...
pass
语句可以用来创建最小类:
>>> class MyEmptyClass:
... pass
...
pass
语句的另一个用处是,当需要编写新的代码时,使用它作为函数或者条件体的位置占位符,这为作者在更加抽象的层次思考问题提供了便利。pass
被默默地忽视:
>>> def initlog(*args):
... pass # Remember to implement this!
...
4.6 Defining Functions
以下是打印任意边界斐波那契数列的函数:
>>> def fib(n): # write Fibonacci series up to n
... """Print a Fibonacci series up to n."""
... a, b = 0, 1
... while a < n:
... print(a, end=' ')
... a, b = b, a+b
... print()
...
>>> # Now call the function we just defined:
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
关键字def
引入函数定义,必须在后面跟上函数名字和形参列表。构成函数体的语句另起一行,必须缩进。
函数体的第一条语句可以选择性使用字符串作为函数的文档字符串,或者docstring
。(更多关于文档字符串参见 Documentation Strings) 许多工具使用文档字符串自动生成在线或者打印文档,或者允许使用者交互地浏览代码;在代码中写文档字符串是一个好的实践,因此需要将其作为习惯。
函数的执行引入了函数局部变量使用的新符号表。更准确的说,所有在函数中赋值变量都将值存储在局部符号表中;变量引用首先在局部符号表中查找,然后是封闭函数的局部符号表,然后是全局符号表,最后是built-in符号表。因此,在函数中全局变量可以被引用,但是不能直接赋予新值(除非使用global
语句声明)。
当函数被调用时,实参也被引入被调用函数的局部符号表中。因此,参数按照按值调用的方式传递(这里的值总是对象引用,而不是对象的值。)[1] 当函数调用其他函数时,为调用会产生新的局部符号表。
函数定义在当前符号表中引入函数名。函数名引用的值拥有一个被解释器识别为“用户自定义函数”的类型。这个值也可以赋值给其他名字,然后使用这个名字来调用函数。这种方式是函数重命名机制:
>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89
拥有其他语言经验的程序员,可能会认为fib
不是一个方法而是一个过程,因为它没有返回值。事实上,没有返回语句的函数确实返回了值,而且是相当烦人的值,这个值被称为None
(built-in名字)。如果None
值是唯一要写的值,那么写的时候通常会被解释器忽视。使用print()
可以看到打印的None
值:
>>> fib(0)
>>> print(fib(0))
None
想要定义返回斐波那契数列数字而不是打印数字的函数非常简单:
>>> def fib2(n): # return Fibonacci series up to n
... """Return a list containing the Fibonacci series up to n."""
... result = []
... a, b = 0, 1
... while a < n:
... result.append(a) # see below
... a, b = b, a+b
... return result
...
>>> f100 = fib2(100) # call it
>>> f100 # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
这个例子也示范了Python的一些新特性:
return
语句从函数中返回一个值。不带任何表达式的return
语句返回None
。函数结束时也会返回None
。- 语句
result.append(a)
调用列表对象result
的方法。方法是属于对象的函数,并且以obj.methodname
命名,这里obj
是一个对象(或许是返回对象的表达式),methodname
是定义在obj
所属类型中的函数名。不容的类型定义了不同的函数。不同类型中的函数可以拥有相同的名字而不会导致二义(使用类,自定义新的对象类型和函数是可行的,参见Classes
) 这个例子中的方法append()
定义在列表对象中;该方法在列表末尾添加新元素。这个例子中等同于result = result + [a]
,但是append()
方法更加高效。
4.7 More on Defining Functions
定义函数可以使用许多参数,有三种形式,可以将其结合使用。
4.7.1 Default Argument Values
为一个或者更多参数指定默认值是最有用的。这种方式创建的函数,可以使用比定义所需更少的参数来调用。例如:
def ask_ok(prompt, retries=4, reminder='Please try again!'):
while True:
ok = input(prompt)
if ok in ('y', 'ye', 'yes'):
return True
if ok in ('n', 'no', 'nop', 'nope'):
return False
retries = retries - 1
if retries < 0:
raise ValueError('invalid user response')
print(reminder)
这个函数可以用以下方式调用:
- 指定唯一的强制参数:
ask_ok('Do you really want to quit?')
- 指定其中一个可选参数:
ask_ok('OK to overwrite the file?', 2)
- 或者指定所有参数:
ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
这个例子也介绍了关键字in
,用来测试序列是否包含具体的值。
参数的默认值在函数定义时求值,因此:
i = 5
def f(arg=i):
print(arg)
i = 6
f()
会打印5。
重要提示: 参数默认值只被求值一次。当默认参数是诸如列表,字典或者其他大多数类实例般的可变对象时,会有很大不同。例如,以下函数累积后续调用传递的实参:
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
会打印:
[1]
[1, 2]
[1, 2, 3]
如果不希望默认参数被随后的调用共享,可以使用如下的函数代替:
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
4.7.2 Keyword Arguments
也可以使用如kwarg = value
形式的关键字参数调用函数。例如如下函数:
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
print("-- This parrot wouldn't", action, end=' ')
print("if you put", voltage, "volts through it.")
print("-- Lovely plumage, the", type)
print("-- It's", state, "!")
接受一个必须参数(voltage
)以及三个可选参数(state, action
和type
)。这个函数可以使用以下的任意方式调用:
parrot(1000) # 1 positional argument
parrot(voltage=1000) # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM') # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000) # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump') # 3 positional arguments
parrot('a thousand', state='pushing up the daisies') # 1 positional, 1 keyword
但是下面的调用方式都是非法的:
parrot() # required argument missing
parrot(voltage=5.0, 'dead') # non-keyword argument after a keyword argument
parrot(110, voltage=220) # duplicate value for the same argument
parrot(actor='John Cleese') # unknown keyword argument
函数调用中,关键字参数必须在位置参数之后。所有传递的关键字实参必须匹配函数接受的其中一个形参(例如,actor
对于函数parrot
来说就不是一个合法的关键字参数),其顺序不是重要的。也包括非可选形参(例如parrot(voltage = 1000)
也是合法的)。任意形参都不会接受两次实参值。以下示例就是由于这条限制而调用失败的:
>>> def function(a):
... pass
...
>>> function(0, a=0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for keyword argument 'a'
当最后一个的形参是**name
形式时,这个参数接收字典(参见 Mapping Types-dict
),这个字典包含不能与形参匹配的关键字实参。关键字参数可以与形如*name
的形式参数结合,这种形式参数以元组接收不能与形参匹配的位置参数。(*name
必须在**name
之前)。例如,以下函数:
def cheeseshop(kind, *arguments, **keywords):
print("-- Do you have any", kind, "?")
print("-- I'm sorry, we're all out of", kind)
for arg in arguments:
print(arg)
print("-" * 40)
for kw in keywords:
print(kw, ":", keywords[kw])
可以像下面一样调用:
cheeseshop("Limburger", "It's very runny, sir.",
"It's really very, VERY runny, sir.",
shopkeeper="Michael Palin",
client="John Cleese",
sketch="Cheese Shop Sketch")
当然会如下打印:
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch
注意关键字参数打印的顺序保证是与其在函数调用时参数提供的顺序相同的。
4.7.3 Arbitrary Argument Lists
最后,最不常用的是让函数可以使用任意数量的参数调用。这些参数会被包装在元组中(参见 Tuples and Sequences)。在可变数量形参之前,可以有零个或者更多普通参数:
def write_multiple_items(file, separator, *args):
file.write(separator.join(args))
通常,可变参数是形参列表中的最后一个,因为可变参数接收了传递给函数的所有剩余实参。出现在*args
后面的任意任意形参都是keyword-only
参数(强制关键字参数),意味着他们只能作为关键字参数使用,而不能用作位置参数。
>>> def concat(*args, sep="/"):
... return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'
4.7.4 Unpacking Argument Lists
参数已经封装在列表或者元组中,但是当调用需要分离的位置参数的函数时,需要拆包元组或者列表。例如,built-in函数range()
需要分离的start
和stop
参数。如果它们不是分离的,可以在函数调用时使用*
操作符来从列表或者元组中拆包(译注:事实证明,字符串也是可以被拆包的):
>>> list(range(3, 6)) # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args)) # call with arguments unpacked from a list
[3, 4, 5]
以同样的方式,使用**
操作符,字典可以传递关键字参数:
>>> def parrot(voltage, state='a stiff', action='voom'):
... print("-- This parrot wouldn't", action, end=' ')
... print("if you put", voltage, "volts through it.", end=' ')
... print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !
4.7.5 Lambda Expressions
可以使用关键字lambda
创建小的匿名方法。这个函数返回其两个参数之和:lambda a, b: a + b
。Lambda方法可以在所有需要函数对象的地方使用。句法上,它们在被限制在一个单个表达式中。语法上,它们只是正常函数定义的语法糖而已。像内嵌函数定义一样,lambda方法也可以引用外部作用域的变量:
>>> def make_incrementor(n):
... return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43
上面的例子使用lambda表达式来返回一个函数。其他的用法是将这个小函数为参数传递:
>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
4.7.6 Documentation Strings
这里有一些关于文档字符串的内容和格式的约定。
第一行应该是对象用途短小而简明地总结。简洁起见,不应该详细描述对象的名字和类型,因为可以通过其他途径了解(除非名字恰好是描述方法操作的动词)。这一行应该以大写字母开始并以句号结束。
如果文档字符串有多行,第二行应该空出来,从视觉上把总结和其他的描述分开来。剩余行应是一个或者多个描述对象的调用约定及其副作用的段落。
Python分析程序不会去掉多行字符串中的缩进,因此如果必要的话,文档处理工具自己必须去掉缩进,这遵循以下约定:第一行字符串后面的第一个非空行决定整个文档字符串缩进的数量。(因为第一行紧挨着它的起始引号,因此表面上看不出其缩进,所以不能使用第一行作为标准)留白“相当于”是字符串的起始缩进。每一行都不应该有缩进,如果有缩进的话,所有的留白都应该清除掉。留白的长度应当等于扩展制表符的宽度(通常是8个空格)。
以下是多行文档字符串的示例:
>>> def my_function():
... """Do nothing, but document it.
...
... No, really, it doesn't do anything.
... """
... pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.
No, really, it doesn't do anything.
4.7.7 Function Annotations
方法注解 是描述用户自定义方法使用的类型的元信息,是完全可选的(参见PEP 484获取更多信息)。
注解作为字典存储在方法属性__annotations__
中,并且对方法的其他部分没有任何影响。参数注解定义在参数名字后的冒号后面,紧跟计算注解值得表达式。返回注解使用->
定义,紧跟表达式,在参数列表和指示def
语句结束的冒号中间。下面的例子有一个位置参数,关键字参数以及返回值的注解:
>>> def f(ham: str, eggs: str = 'eggs') -> str:
... print("Annotations:", f.__annotations__)
... print("Arguments:", ham, eggs)
... return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'
4.7.8 Intermezzo: Coding Style
现在需要写更长更复杂的Python程序,是时候谈论编码风格了。大多数语言可以以不同的风格编写(更简洁的说是格式化),一些编码风格比其他的更加可读。让其他人更容易读懂你的代码一直都是很重要的,养成良好的编码风格很重要。
对于Python来说,PEP 8被作为风格规范指导,许多项目都在使用这种规范。这个规范促成了一种非常易读和养眼的代码风格。在某种程度上每一个Python开发者都应该阅读它,以下是从这个规范中提取的重要内容:
- 使用4个空格作为缩进,而不是tab
4个空格是小的缩进(允许更深的嵌套深度)和大缩进(更易读)之间的良好折中方案。Tabs会导致困扰,最好弃用 - 换行以保证每一行不会超过79个字符
这在小的显示器上非常有用,也可在大的显示器中分屏显示多个文件 - 使用空行分开方法和类,以及函数中的大代码块
- 如果可能,注释独占一行
- 使用文档字符串
- 在操作符的两端以及顿号后面使用空格,但是不要在括号内侧使用:
a = f(1, 2) + g(3, 4)
- 统一命名类以及函数;约定使用
CamelCase
(驼峰)命名类,使用lower_case_with_underscores
(小写带下划线)命名方法和函数。总是使用self
作为方法的第一个参数名(参见A first Look at Classes了解更多关于类和方法的信息) - 如果代码要在国际环境中使用,不要使用自己喜爱的独特编码方式。Python的默认编码
UTF-8
或者甚至普通的ASCII
编码都能在任何情况下起作用。 - 同样的,不要在标识符中使用非
ASCII
字符,除非是不同语种的人会阅读和维护代码。
Footnotes
[1] 事实上,使用引用传递会描述更好,因为如果传递的是可变对象,被调用者对对象的修改对调用者可见(如在列表中插入新的项)