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的序列的项的合法索引。可以让范围从另一个数字开始,或者指定一个不同的增量(即使是负数,有时这称为“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() 函数,请参见 循环技术

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

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

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

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

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

后面我们将看到更多的函数返回iterables和接受iterables作为参数。

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

很容易写一个返回Fibonacci系列数字列表的函数,而不是打印它:

>>> 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解析器不会在Python中从多行字符串字面值中删除缩进,因此处理文档的工具必须剥离缩进。使用以下约定即可,字符串第一行之后的第一个非空行确定整个文档字符串的缩进量。(我们不能使用第一行,因为它通常与字符串的开头引号相邻,所以它的缩进在字符串字面值中不明显。)这个缩进的空格“等价”然后从字符串的所有行的开始被去除。缩进较少的行不应该出现,但如果它们出现,它们的所有前导空白都应该被删除。扩展标签后,应测试空格的等效(通常为8个空格)。

下面是一个多行docstring的例子:

>>> 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]实际上,调用对象引用将是一个更好的描述,因为如果一个可变对象被传递,调用者将看到被调用者对它(插入列表中的项目)的任何更改。