4. 流程控制语句

IamZS 91 0

除了刚刚介绍过的 while 语句,Python 还支持在其他语言中常见的流程控制语句,以及一些混杂的语句(with some twists)。

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 或者 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']

如果(不像上面那样做而是)使用 for w in words: 的话,将会创造一个无限序列,一遍又一遍地将 defenestrate 插入到序列中。

4.3. range() 函数

如果你需要迭代一个数字序列,内建函数 range() 就能派上用场了。它可以生成算术级数:

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

给定的终点不会包含在生成的序列中;range(10) 生成 10 个值,分别为长度为 10 的序列各元素的合法索引。也可以让 range 函数从另一个数开始(缺省默认为 0),或者指定一个不同的增量(甚至是负数也可以,有时候被称为“步进值”):

range(5, 10)
   5, 6, 7, 8, 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() 返回的对象在很多方面看上去像是一个列表,但事实上并不是。当你遍历它的时候,它会不断返回预期序列的各项,但它并非直接生成列表,从而节省空间。

我们称这样一个对象为可迭代对象,也就是说,可以作为函数和结构的目标,用来返回连续元素直到没有。我们已经知道,for 语句是这样一种迭代器。list() 是另一个,它可以从可迭代对象创建列表:

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

后面我们将会了解到更多返回可迭代对象并将可迭代对象作为参数的函数。

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

break 语句和 C 语言中的一样,用于跳出最内层的 for 循环或 while 循环。

循环语句可能有一个 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 子句相比,循环语句中的 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 语句什么也不干。当从语法上来说需要有语句但是程序不需要任何动作时,可以使用 pass 语句。比如:

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

这通常用于创建最小类(minimal classes):

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

另一个使用 pass 语句的地方就是当你编写新代码时作为函数和控制体的占位符,允许你专注于在更高抽象层上思考。pass 语句会被默默忽略掉:

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

4.6. 定义函数

我们可以编写一个函数来生成任意范围内的斐波那契数:

>>> 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 引入 definition 函数。其后必须紧跟函数名和用括号表明的形式参数列表。函数体语句从一行开始,且必须缩进。

函数体的第一行是一个可选的字符串文本;该字符串是函数的文档字符串或称 docstring。(更多关于文档字符串的内容可以在文档字符串这一节找到。)有些工具使用这些文档字符串来生成在线或打印的文档,或者让用户交互式浏览代码;在编写的代码中引入文档字符串是非常好的做法,所以尽量使之成为习惯。

函数的执行会为其引入一个局部变量表。更确切地说,函数中的变量赋值都是将值存储在局部变量表中;当引用变量的时候首先在局部变量表中查找,(如果没有),然后在上层函数(enclosing functions,不确定)的局部变量表中查找,(如果没有),再在全局变量表中查找,(如果没有),最后在内置的变量名称中查找。因此,在函数内不能直接给全局变量赋值(除非是在 global 语句中),尽管它们可以被引用到。

当函数被调用时,传入函数的实际参数被引入到函数的局部变量表中;因此,参数传递通过传值调用(这里的值始终是对象的引用而不是对象的值)。当一个函数调用另一个函数时,会为其创建一个新的局部变量表。

函数定义将函数名引入到当前符号表内。函数名的值的类型被解释器识别为用户自定义函数。这个值可以赋值给另一个变量,然后该变量也可以用作函数。这是通用的重命名机制:

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

如果了解其他编程语言,你可能会反对说 fib 并不是一个函数而是一个过程(?procedure),因为它不返回任何值。事实上,没有 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 来定义你自己的对象类型和方法,参见:)例子中的 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 <module>
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)
    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. 可变参数

函数可以用可变参数调用。这些参数被封装在一个元组中(参见元组和序列)。在可变参数之前,可能有 0 个或多个一般参数。

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() 需要单独的 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 表达式

可以用 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 3107 and 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 个空格是介于小缩进(允许更深的嵌套)和大缩进(更易于阅读)之间的一种很好的折中方案。制表符会造成混乱,最好不要用。

  • 折行确保一行不会超出 79 个字符。

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

  • 使用空行来隔开函数和类,以及函数内的代码块。
  • 可能的话,注释应单独成行。
  • 使用文档字符串。
  • 在操作符两边和逗号后加空格,但请不要直接在包围结构(bracketing constructs)内添加:a = f(1, 2) + g(3, 4)
  • 类和函数的命名风格应一致;传统上使用驼峰方式(CamelCase)来命名类,使用小写字母加下划线(lower_case_with_underscores)来命名函数和方法。总是使用 self 作为方法的第一个参数名(查看初识类了解更多关于类和方法)。
  • 如果您的代码要在国际环境中使用,请不要使用花哨的编码方法。Python 的默认编码方法 UTF-8,或者是 ASCII 在任何情况下都是最好的。
  • 同理,哪怕有一丁点的可能性说不同语言的人会阅读和维护你的代码,就不要在标识符中使用非 ASCII 字符。

发表评论
表情 图片 链接 代码