4. 流程控制语句

除了 while 语句,Python还拥有在其他语言里常见的控制语句,以及一些好玩的语句。

4.1.if 语句

也许最常见的语句类型是 if 语句.例如:

>>> x = int(input("Please enter an integer: "))
Please enter an integer: 42
>>> if x < 0:
...     x = 0
...     print('Negative changed to zero')
... elif x == 0:
...     print('Zero')
... elif x == 1:
...     print('Single')
... else:
...     print('More')
...
More

可以有0个或多个 elif 块,,并且 else 块是可选的。关键字 ‘elif‘ 比 ‘else if’ 短,,并且可以避免过度缩进。一个if ... elif ... elif ...序列 是对其他语言的switch or case 语句的替代方案。

4.2. for 语句

Python 的 for 语句与你在 C 或者 Pascal 中使用的有一点区别。和常见的等差数列迭代(如 Pascal 中)或让用户能够自定义迭代步骤和停止条件(如 C)不一样,Python 的 for 语句可以按照元素出现的顺序迭代任何序列(列表或字符串)。例如:

>>> # Measure some strings:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

如果要在循环内修改正在迭代的序列(例如,复制所选的项目),建议首先创建原序列的拷贝。迭代序列不会隐式地创建副本。 使用切片就可以很容易地做到:

>>> for w in words[:]:  # Loop over a slice copy of the entire list.
...     if len(w) > 6:
...         words.insert(0, w)
...
>>> words
['defenestrate', 'cat', 'window', 'defenestrate']

4.3. range() 函数

如果你确实需要遍历一个数字序列,内置函数 range() 会派上用场。它生成算术级数:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

给定的终点不会包含在生成的序列中; range(10)会生成10个值,即长度为10的序列的项的合法索引。也可以让 range 函数从另一个数值开始,或者可以指定一个不同的步进值(甚至是负数,有时这也被称为‘步长’):

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() 函数,请参见 循环技术

如果你只打印 range,会出现奇怪的结果:

>>> print(range(10))
range(0, 10)

在很多方面由 range() 返回的对象的行为就像它是一个列表,但事实上它不是。它是一个对象。当您遍历它时, 它会返回所需的序列连续项。但它并不真的生成了列表,从而节省了空间。

我们说这样一个对象是iterable,就是说,它适合作为一个函数和结构的目标,而函数期望从中获得连续的项目,直到耗尽。我们已经看到 for 语句是这种 迭代器list() 函数是另一个;它从可迭代量创建列表︰

>>> list(range(5))
[0, 1, 2, 3, 4]

后面我们会看到更多返回可迭代对象和以可迭代对象作为参数的函数。

4.4. breakcontinue语句,以及循环中else子句

break语句和 C 中的类似,用于跳出最近的 forwhile 循环。

循环语句可以有一个 else 子句;当(for)循环迭代完整个列表或(while)循环条件变为假,而非由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 语句的 。)

与在if语句中的用法相比,循环中的else子句与try语句的else子句有更多的共同点:try语句的else子句在未出现异常时运行,循环的else子句在未出现break时运行。更多关于try语句和异常的内容,请参见 处理异常

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 语句

pass 语句什么也不做。当语法上需要语句但程序不需要动作时,可以使用它。例如:

>>> while True:
...     pass  # Busy-wait for keyboard interrupt (Ctrl+C)
...

这通常用于创建最小的类:

>>> class MyEmptyClass:
...     pass
...

另一个使用pass的地方是编写新代码时作为函数体或控制体的占位符,这让你在更抽象层次上思考。pass语句将被默默地忽略:

>>> def initlog(*args):
...     pass   # Remember to implement this!
...

4.6. 定义函数

我们可以创建一个函数,将斐波纳契(Fibonacci)序列写入任意边界:

>>> 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(更多关于 docstrings 的内容可以在 文档字符串一节中找到。)有工具使用 docstrings 自动生成在线的或可打印的文档,或者让用户在代码中交互浏览;在您编写的代码中包含 docstrings 是很好的做法,所以让它成为习惯吧。

执行 一个函数会引入一个用于函数的局部变量的新符号表。更确切地说,函数中的所有的赋值都是将值存储在局部符号表;而变量引用首先查找局部符号表,然后是上层函数的局部符号表,然后是全局符号表,最后是内置名字表。因此,在函数内部无法给一个全局变量直接赋值(除非在一个 global 语句中命名),虽然可以引用它们。

当函数被调用时候,函数调用的实际参数被引入到被调用函数的局部(本地)符号表中;因此,参数传递通过 传值调用 (这里的 始终是对象的 引用,不是对象的值)。[1]一个函数调用另一个函数时,会为本次调用创建一个新的局部符号表。

函数定义会在当前符号表内引入函数名。函数名对应的值的类型是解释器可识别的用户自定义函数。此值可以分配给另一个名称,然后该名称也可作为函数。这是通用的重命名机制:

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

如果你使用过其他语言,你可能会反对说:fib 不是一个函数,而是一个过程(子程序),因为它并不返回任何值。事实上,没有return语句的函数也返回一个值,尽管是一个很无聊的值。此值被称为 None(它是一个内置的名称)。如果 None只是唯一的输出,解释器通常不会打印出来。如果你真的想看到这个值,可以使用 print 语句:

>>> 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如果一直执行到整个函数结束还没有碰到 return 语句,也返回 None
  • 语句 result.append(a) 调用了列表result的一个 方法。方法是“属于”一个对象且名字叫做obj.methodname的函数,其中obj是某个对象(可能是个表达式),methodname是由该对象类型定义的方法的名称。不同类型定义了不同的方法。不同的类型的方法可以具有相同的名称而不引起歧义。(可以使用classes定义你自己的对象类型和方法,参见Classes)示例中显示的append() 方法是给列表对象定义的;它添加一个新的元素到列表的末尾。在这个示例中,它等同于result = result + [a],但是它更高效。

4.7. 更多关于定义函数

可以定义具有可变数目的参数的函数。有三种函数形式,可以结合使用。

4.7.1. 默认参数值

最有用的形式是指定一个或多个参数的默认值。这种方法创建的函数被调用时,可以带有比定义的要少的参数。例如:

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. 关键字参数

函数还可以用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, and 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 ?
TypeError: function() got multiple values for keyword argument 'a'

如果在最后存在一个**name形式的形式参数,它将接收一个字典(参见映射类型——字典),这个字典包含除形式参数之外的所有关键字参数。它可以与*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)
    keys = sorted(keywords.keys())
    for kw in keys:
        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.
----------------------------------------
client : John Cleese
shopkeeper : Michael Palin
sketch : Cheese Shop Sketch

注意,在打印关键字参数内容之前,它的名称列表是通过排序字典的keys()方法得到的关键字创建的;如果不这样做,打印出来的参数顺序将是未定义的。

4.7.3. 任意参数的列表

最后,最不常用的场景是指明某个函数可以被可变个数的参数调用。这些参数将被封装在一个元组中(参见元组和序列)。在可变数量的参数之前,可能出现零个或多个正常参数。

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

通常,这些可变的参数将位于形式参数列表的最后面,因为它们将剩下的传递给函数的所有输入参数都包含进去。出现在*args之后的任何形式参数都是“非关键字不可”的参数,意味着它们只能用作关键字参数而不能是位置参数。

>>> 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. 解包参数列表

当传递的参数已经是一个列表或元组时,情况与之前相反,你要分拆这些参数,因为函数调用要求独立的位置参数。例如,内建的range()函数期待单独的startstop参数。如果它们不能单独地获得,可以编写带有*操作的函数调用,来从一个列表或元组分拆出参数:

>>> 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 表达式

可以使用 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. 文档字符串

下面是一些关于文档字符串内容和格式的惯例。

第一行永远应该是对象用途的简短、精确的总述。为了简单起见,不应该明确的陈述对象的名字或类型,因为这些信息可以从别的途径了解到(除非这个名字碰巧就是描述这个函数操作的动词)。这一行应该以大写字母开头,并以句号结尾。

如果在文档字符串中有更多的行,第二行应该是空白,在视觉上把摘要与剩余的描述分离开来。以下各行应该是一段或多段描述对象的调用约定、 其副作用等。

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. 函数注解

函数注解是关于用户定义的函数使用的类型的元数据信息,它们是完全可选的(更多信息参见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.8. 插曲:代码风格

若要编写更长更复杂的 Python 代码,是时候谈一谈 编码风格了大部分语言都可以有多种(比如更简洁,更格式化)写法,有些写法可以更易读。让你的代码更具可读性,而良好的编码风格对此有很大的帮助。

对Python, PEP 8 已经成为多数项目遵循的代码风格指南;它推动了一种非常易于阅读且赏心悦目的编码风格。每个Python开发者都应该找个时间读一下; 以下是从中提取出来的最重要的一些点:

  • 使用 4 个空格的缩进,不要使用制表符。

    4 个空格是小缩进(允许更深的嵌套)和大缩进(易于阅读)之间很好的折衷。制表符会引起混乱,最好弃用。

  • 折行以确保其不会超过 79 个字符。

    这有助于小显示器用户阅读,也可以让大显示器能并排显示几个代码文件。

  • 使用空行分隔函数和类,以及函数内的大块代码。

  • 如果可能,注释单独成行。

  • 使用文档字符串。

  • 在操作符两边和逗号之后加空格, 但不要直接在左括号后和右括号前加: a = f(1, 2) + g(3, 4).

  • 类和函数的命名风格要一致;传统上使用 CamelCase (驼峰风格)命名类 而用 lower_case_with_underscores(小写字母加下划线)命名函数和方法。方法的第一个参数名称应为 self (查看 初识类 以获得更多有关类和方法的规则)。

  • 如果您的代码要在国际环境中使用,不要使用花哨的编码。Python 默认的 UTF-8 或者 ASCII 在任何时候都是最好的选择。

  • 同样,只要存在哪怕一丁点可能性有使用另一种不同语言的人会来阅读或维护你的代码,就不要在标识符中使用非 ASCII 字符。

脚注

[1]事实上,  按对象引用传递   可能是更恰当的说法,因为如果传递了一个可变对象,调用函数将看到任何被调用函数对该可变对象做出的改变(比如添加到列表中的元素)