//文章

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

可以有零个或多个 elif 部分,else 部分是可选的。关键字 'elif' 是 ?'else if' 的简写,可以有效避免过深的缩进。if... elif ... elif ... 序列用于替代其它语言中 switchcase 语句。

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()len() ,如下所示:也可以让 range 函数从另一个数值开始,或者可以指定一个不同的步进值(甚至是负数,有时这也被称为‘步长’):

range(5, 10)
5 through 9range(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()返回的对象的行为在很多方面很像一个列表,但实际上它并不是列表。当你迭代它的时候它会依次返回期望序列的元素,但是它不会真正产生一个列表,因此可以节省空间。

我们把这样的对象称为可迭代的,也就是说,它们适合被一些函数和构造器所利用,这些函数和构造器期望可以从可迭代对象中获取连续的元素,直到没有新的元素为止。 我们已经看到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 语句。)

与循环一起使用的 else 子句更类似于 try 语句的 else 子句而不是 if 语句的 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.定义函数

我们可以创建一个生成任意上界菲波那契数列的函数:

>>> 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。函数直接结束后也返回None
  • 语句 result.append(a) 调用列表对象result 的一个 方法。方法是‘隶属于'某个对象的函数,被命名成 obj.methodname 的形式,其中 obj是某个对象 (或是一个表达式),methodname是由对象类型定义的方法的名称。不同类型定义了不同的方法。不同类型的方法可能具有相同的名称,而不会引起歧义。(也可以使用 class 定义你自己的对象类型和方法,请参见)本例中所示的 append() 方法是list 对象定义的。它在列表的末尾添加一个新的元素。在本例中它等同于result = result?+[a],但效率更高。

4.7.更多关于定义函数

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

4.7.1.默认参数值

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

def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
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 OSError('uncooperative user')
print(complaint)

这个函数可以通过几种方式调用:

  • 只给出强制参数: 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 Lprint(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, actiontype)。可以用下列任意一种方式调用这个函数:

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 必须出现在 ** 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参数后面的任何形式参数都是‘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. 参数列表的分拆

当传递的参数已经是一个列表或元组时,情况与之前相反,你要分拆这些参数,因为函数调用要求独立的位置参数。例如,内置的 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. 函数注释

函数注释是可选的元数据信息,用于描述自定义函数中的类型 (see 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 个字符。
    这有助于小显示器用户阅读,也可以让大显示器能并排显示几个代码文件。
  • 使用空行分隔函数和类,以及函数内的大块代码。
  • 如果可能,注释独占一行。
  • 使用 docstrings。
  • 运算符周围和逗号后使用空格,但是括号里侧不加空格: a = f(1, 2) + g(3, 4)
  • 一致地命名您的类和函数;常见的做法是命名类的时候使用驼峰法,命名函数和方法的时候使用小写字母+下划线法。始终使用self作为第一个方法参数的名称 (请参见类的初识更多的关于的类和方法)。
  • 不要使用花哨的编码,如果您的代码只在国际环境中使用。Python默认的UTF-8,或者纯 ASCII 在任何情况下永远工作得最好。
  • 同样的,如果讲其它语言的人很少有机会阅读或维护你的代码,不要使用非ASCII字符作为标识符。

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

0 0

发表评论