4. 深入 Python 流程控制

除了刚刚介绍的 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 或 case 语句。

4.2. for 语句

Python 中for 语句有点不同于 C 和 Pascal 中的 for 语句。Python 的 for 语句按照项目在序列中出现的顺序迭代任何序列(列表或字符串),而不是总是迭代数学的算术级数(如 Pascal 中),或者让用户能够定义迭代步骤和停止条件(如 C),例如(没有双关语):

>>> # 测量字符串:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

如果你需要修改序列在循环内的迭代(例如复制所选项目),建议你先复制。迭代序列操作并不会隐式地复制。切片方法使这一操作特别方便:

>>> for w in words[:]:  # 循环遍历整个列表的切片副本。
...     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(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() 函数更方便,可以看 循环技术.

如果你直接打印一个区间的话,会发生奇怪的事情:

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

在很多方面, range() 返回的对象的行为像列表,但实际上它不是。它是一个对象,当你迭代它的时候,会连续的返回整个序列的项目,但不会真的创建列表,从而节省空间。

我们称这样的对象为 可迭代的 ,也就是说,它很适合于当作函数或者构造函数的目标,它们期望从这里可以获得连续的项目直到耗尽。我们已经看到 for 语句是一个 迭代器list() 函数是另一个;它可从可迭代对象中创建列表:

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

稍后,我们会看到更多返回可迭代对象和将可迭代对象当作参数的函数。

4.4. break 和 continue 语句,以及循环上的 else 子句

break 语句,类似于 C ,会打破 for 或 while 循环的最内层。

循环语句可能有 else 子句;它会在列表耗尽(用  for )从而终止循环或者条件为假(用  while )的时候被执行,而不是循环被 break 语句终止的时候;这被下面的这个查找素数的循环例证了:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         # 没有找到一个因数导致的循环失败
...         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

(是的,这是正确的代码。密切关注: for 循环的 else 子句,不是 if 语句。)

当在循环使中使用 else 子句时,与其说很类似于if 语句,不如说更类似于 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  # 等待键盘中断(Ctrl+C)
...

通常也用于创建小类的时候:

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

其他地方, pass 可以在你处理新代码的时候,用作函数或者条件体的占位符,从而让你继续思考更抽象层级的事情。 pass 被默默地忽略了:

>>> def initlog(*args):
...     pass   # 记住实现它!
...

4.6. 定义函数

我们可以创建一个能打印出任意项的斐波那契数列的函数:

>>> def fib(n):    # 将斐波那契数列打印到 n
...     """将斐波那契数列打印到 n"""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # 调用上面定义的函数
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

关键字 def 引入了一个函数 定义 。其后面必须跟随有函数的名称以及用括号包起来的一系列参数。构成函数体的语句从下一行开始,并且必须缩进。

函数体的第一个语句可以是一个字符串常量,这个字符串常量就是这个函数的文档字符串,或者说是 docstring 。(更多关于文档字符串的内容可参考章节 Documentation Strings 。)有很多工具可以用于在线或者可打印文档的自动化生成,或者可以让用户交互地在代码中浏览文档;在代码中写文档字符串是比较好的实践,所以请养成写文档字符串的习惯。

函数的 执行 引入了一个新的符号表用于存储函数的局部变量。更准确地说,在函数内的所有变量赋值都会被存储到这张局部符号表中;所以在查找一个变量的引用时,会先查找局部符号表,然后查找闭包函数的局部符号表,接着是全局符号表,最后才是内置名称表。因此,尽管可能在函数中引用全局变量,但在函数中无法对全局变量直接进行赋值(除非用 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 ,如果你真的想打印出 None ,你可以使用 print()

>>> fib(0)
>>> print(fib(0))
None

写一个返回包含斐波那契数列的列表的函数比写一个打印斐波那契数列的函数要简单:

>>> def fib2(n):  # 返回斐波那契数列小于 n 的项
...     """返回包含斐波那契数列小于 n 的项的列表"""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # 看上面的解释
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # 调用函数
>>> f100                      # 输出结果
[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 表示该对象类型定义中方法的名字。不同的类型定义了不同的方法。不同类型的方法的名字可以是相同的且不会产生歧义。(你可以使用 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, onlyyes or no!')

上述例子顺便也提及了 in 关键字。它是用来测试某个特定值是否在一个序列中。

默认值是在定义函数时的“定义过程中” (defining )的范围内评估的(函数参数默认值是个变量的话,要根据函数定义前变量的值来确定参数默认值), 所以,

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 ) 和三个可选参数( stateaction, 和 type )。这个函数下方式调用:

parrot(1000)                                          # 一个位置参数
parrot(voltage=1000)                                  # 一个关键字参数
parrot(voltage=1000000, action='VOOOOOM')             # 2个关键字参数
parrot(action='VOOOOOM', voltage=1000000)             # 2个关键字参数
parrot('a million', 'bereft of life', 'jump')         # 3个位置参数
parrot('a thousand', state='pushing up the daisies')  # 一个位置参数,一个关键字参数

但是下列的所有调用方式都是无效的:

parrot()                     # 必选参数缺失
parrot(voltage=5.0, 'dead')  # 非关键字参数在关键字参数后面
parrot(110, voltage=220)     # 同一参数重复赋值
parrot(actor='John Cleese')  # 未知关键字参数

在函数调用中,关键字参数必须遵循参数位置。传递的所有关键字参数必须跟函数接受的其中一个参数相匹配。(例如: 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. 可变参数

最后,最不常用的指定参数的选项是可变数量的参数。这些参数将被组装成一个元组(参见  元组和序列) 。在可变参数之前,可能会出现零个或多个正常参数。

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))            # 正常利用参数调用函数
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # 从列表中提取参数来调用函数
[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个空格)。

以下是个多行文档字符串的例子:

>>> def my_function():
...     """只要写文档,其他啥都别做。
...
...     确实,它也啥都不做。
...     """
...     pass
...
>>> print(my_function.__doc__)
只要写文档,其他啥都别做。

    确实,它也啥都不做。

4.7.7. 函数注解

函数注解 (Function annotations)应用于用户自定义的函数,可使用的类型是完全可选的元数据 (参考 PEP 3107和 PEP 484 获取更多信息)。

注解(Annotations)是以字典的形式存放在函数的  __annotations__  属性中,并且不对函数有任何影响。参数注解 (Parameter annotations) 是由参数名称后面加上冒号来定义的,后面紧跟一个描述,来表示注解的值。 返回注解 (Return 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 代码了。 是时候可以谈谈代码风格了。大多数编程语言可以使用不同的代码风格编写(就是格式化); 有的可读性比其他的强。使用一种不错的代码风格可以帮助别人更好的阅读你的代码。

 PEP 8 是大多数 Python 项目使用的代码风格指南。它提供了高可读性和养眼的代码风格。每一个 Python 开发者都应该阅读它,这里列出了其中的一些重点:

  • 缩减使用四个空格而不是制表符

    四个空格缩进比更少空格(运行跟多的嵌套深度)或者更多空格(更容易阅读)的缩进要好。 制表符会带来歧义,所以最好不要用它

  • 每行不要超过79个字符

    这可以帮助显示器较小的用户与帮助显示器较大的用户同屏显示多个文件。

  • 使用空行分隔函数、类或者函数内较大的代码段。

  • 尽量将注释和代码放在一起。

  • 用 docstrings。

  • 用在操作符前后和逗号之后加空格,但是括号之内不需要: a= f(1, 2) + g(3, 4).

  • 一致性的命名你的类与函数;惯例是用 CamelCase 命名类 ,用 lower_case_with_underscores 命名函数和方法。必须使用 self 作为方法的第一个参数(想了解更多请阅读 A First Look at Classes)。

  • 如果你的代码将用于国际化的环境中,请不要使用任何花哨的编码。 Python 默认使用 UTF-8,甚至纯 ASCII 在任何情况下都能最好地工作。

  • 即使说其他语言的人阅读或者维护你的代码的几率很小,也不要使用非 ASCII 字符。

注脚

本文章首发在 LearnKu.com 网站上。
上一篇 下一篇
贡献者:4
讨论数量: 0
发起讨论 只看当前版本


暂无话题~