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’ 的缩写,有助于避免过多的缩进。ifelifelif … 序列是其他语言中 switchcase 语句的替代品。

如果要将相同的值与多个常量进行比较,或者检查特定的类型或属性,您可能还会发现 match 语句很有用。有关更多详细信息,请参见 match 语句

4.2. for 语句

Python 中的 for 语句与您在 C 或 Pascal 中可能习惯的略有不同。Python 的 for 语句不是像在 Pascal 中那样总是迭代一个算术级数,或者像 C 中那样让用户能够定义迭代步长和停止条件,而是迭代任何序列(列表或字符串)中的项目,按照它们在序列中出现的顺序进行。 例如(绝非玩笑)

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

在迭代集合时修改同一集合的代码可能会很难正确编写。 相反,通常更直接的做法是循环遍历集合的副本或创建新集合

# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]

# Strategy:  Create a new collection
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

4.3. range() 函数

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

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

给定的结束点永远不属于生成的序列; range(10) 生成 10 个值,即长度为 10 的序列的项的合法索引。 可以让范围从另一个数字开始,或者指定不同的增量(甚至是负数;有时这称为“步长”)

>>> list(range(5, 10))
[5, 6, 7, 8, 9]

>>> list(range(0, 10, 3))
[0, 3, 6, 9]

>>> list(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,就会发生一件奇怪的事情

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

在许多方面,range() 返回的对象表现得好像它是一个列表,但实际上它不是。它是一个对象,当您迭代它时,它会返回所需序列的连续项,但它不会真正创建列表,从而节省了空间。

我们说这样的对象是 可迭代的,也就是说,适合作为期望从中获取连续项直到供应耗尽的函数和构造的目标。我们已经看到 for 语句是这样一种构造,而一个接受可迭代对象的函数示例是 sum()

>>> sum(range(4))  # 0 + 1 + 2 + 3
6

稍后我们将看到更多返回可迭代对象和接受可迭代对象作为参数的函数。在第 数据结构 章中,我们将更详细地讨论 list()

4.4. breakcontinue 语句

break 语句会跳出最内层的 forwhile 循环

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(f"{n} equals {x} * {n//x}")
...             break
...
4 equals 2 * 2
6 equals 2 * 3
8 equals 2 * 4
9 equals 3 * 3

continue 语句继续循环的下一次迭代

>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print(f"Found an even number {num}")
...         continue
...     print(f"Found an odd number {num}")
...
Found an even number 2
Found an odd number 3
Found an even number 4
Found an odd number 5
Found an even number 6
Found an odd number 7
Found an even number 8
Found an odd number 9

4.5. 循环中的 else 子句

forwhile 循环中,break 语句可以与 else 子句配对。 如果循环在没有执行 break 的情况下完成,则会执行 else 子句。

for 循环中,else 子句在循环完成最后一次迭代后执行,也就是说,如果没有发生中断。

while 循环中,它在循环条件变为假后执行。

在任何类型的循环中,如果循环因 break 而终止,则 不会 执行 else 子句。 当然,其他提前结束循环的方法,例如 return 或引发异常,也会跳过 else 子句的执行。

下面的 for 循环示例说明了这一点,该循环搜索质数

>>> 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 子句:把它想象成与循环内的 if 配对。当循环执行时,它会运行类似 if/if/if/else 这样的序列。if 在循环内部,会被多次遇到。如果条件为真,则会发生 break。如果条件始终不为真,则会执行循环外部的 else 子句。

当与循环一起使用时,else 子句与 try 语句的 else 子句的共同点更多,而不是与 if 语句的 else 子句:try 语句的 else 子句在没有异常发生时运行,而循环的 else 子句在没有发生 break 时运行。有关 try 语句和异常的更多信息,请参阅 处理异常

4.6. 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.7. match 语句

match 语句接受一个表达式,并将其值与一个或多个 case 代码块中给出的连续模式进行比较。这表面上类似于 C、Java 或 JavaScript(以及许多其他语言)中的 switch 语句,但它更类似于 Rust 或 Haskell 等语言中的模式匹配。只有第一个匹配的模式会被执行,并且它还可以从值中提取组件(序列元素或对象属性)到变量中。

最简单的形式是将一个主题值与一个或多个字面量进行比较

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

请注意最后一个块:“变量名” _ 用作通配符,并且永远不会匹配失败。如果没有 case 匹配,则不会执行任何分支。

你可以使用 | (“或”) 在单个模式中组合多个字面量

case 401 | 403 | 404:
    return "Not allowed"

模式可以看起来像解包赋值,并且可以用于绑定变量

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

仔细研究一下!第一个模式有两个字面量,可以被认为是上面显示的字面量模式的扩展。但是接下来的两个模式结合了一个字面量和一个变量,并且变量从主题(point)中绑定一个值。第四个模式捕获两个值,这使得它在概念上类似于解包赋值 (x, y) = point

如果你使用类来构建数据,可以使用类名后跟一个类似于构造函数的参数列表,但具有将属性捕获到变量中的能力

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

你可以将位置参数与某些为其属性提供顺序的内置类(例如数据类)一起使用。你还可以通过在类中设置 __match_args__ 特殊属性来为模式中的属性定义特定位置。如果将其设置为 (“x”, “y”),则以下模式都是等效的(并且都将 y 属性绑定到 var 变量)

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

阅读模式的一种推荐方法是将它们视为你放置在赋值左侧的扩展形式,以了解哪些变量将设置为什么。只有独立的名称(如上面的 var)会被 match 语句赋值。点状名称(如 foo.bar)、属性名称(上面的 x=y=)或类名称(由它们旁边的 “(…)” 识别,如上面的 Point)永远不会被赋值。

模式可以任意嵌套。例如,如果我们有一个短的点列表,添加了 __match_args__,我们可以像这样匹配它

class Point:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

我们可以向模式添加一个 if 子句,称为“守卫”。如果守卫为假,则 match 将继续尝试下一个 case 代码块。请注意,值捕获发生在守卫被评估之前

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

此语句的其他几个关键特性

  • 与解包赋值类似,元组和列表模式具有完全相同的含义,并且实际上匹配任意序列。一个重要的例外是它们不匹配迭代器或字符串。

  • 序列模式支持扩展解包:[x, y, *rest](x, y, *rest) 的工作方式类似于解包赋值。* 之后的名称也可以是 _,因此 (x, y, *_) 匹配至少两个项目的序列,而不绑定其余的项目。

  • 映射模式:{"bandwidth": b, "latency": l} 从字典中捕获 "bandwidth""latency" 的值。与序列模式不同,额外的键会被忽略。还支持像 **rest 这样的解包。(但是 **_ 将是多余的,因此不允许使用。)

  • 可以使用 as 关键字捕获子模式

    case (Point(x1, y1), Point(x2, y2) as p2): ...
    

    将捕获输入的第二个元素作为 p2(只要输入是两个点的序列)

  • 大多数字面量通过相等性进行比较,但是单例 TrueFalseNone 通过标识进行比较。

  • 模式可以使用命名常量。这些必须是点状名称,以防止它们被解释为捕获变量

    from enum import Enum
    class Color(Enum):
        RED = 'red'
        GREEN = 'green'
        BLUE = 'blue'
    
    color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
    
    match color:
        case Color.RED:
            print("I see red!")
        case Color.GREEN:
            print("Grass is green")
        case Color.BLUE:
            print("I'm feeling the blues :(")
    

有关更详细的解释和其他示例,你可以查看以教程格式编写的PEP 636

4.8. 定义函数

我们可以创建一个将斐波那契数列写入任意边界的函数

>>> def fib(n):    # write Fibonacci series less than n
...     """Print a Fibonacci series less than 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。(有关文档字符串的更多信息,请参阅 文档字符串 部分。)有一些工具使用文档字符串自动生成在线或打印文档,或让用户以交互方式浏览代码;在编写的代码中包含文档字符串是一个好习惯,所以养成这个习惯。

函数的执行会引入一个新的符号表,用于函数的局部变量。更准确地说,函数中的所有变量赋值都会将值存储在局部符号表中;而变量引用首先查找局部符号表,然后在封闭函数的局部符号表中查找,然后在全局符号表中查找,最后在内置名称表中查找。因此,全局变量和封闭函数的变量不能在函数内直接赋值(除非对于全局变量,在 global 语句中命名,或者对于封闭函数的变量,在 nonlocal 语句中命名),尽管可以引用它们。

函数调用的实际参数(参数)在调用时引入到被调用函数的局部符号表中;因此,参数是通过按值调用传递的(其中始终是对象的引用,而不是对象的值)。[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 是由对象类型定义的方法的名称。不同的类型定义不同的方法。不同类型的方法可能具有相同的名称,而不会引起歧义。(可以使用定义你自己的对象类型和方法,请参阅 )示例中显示的 append() 方法是为列表对象定义的;它在列表的末尾添加一个新元素。在本例中,它等效于 result = result + [a],但效率更高。

4.9. 关于定义函数的更多内容

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

4.9.1. 默认参数值

最有用的形式是为一个或多个参数指定默认值。 这会创建一个可以使用比定义时更少的参数调用的函数。 例如

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        reply = input(prompt)
        if reply in {'y', 'ye', 'yes'}:
            return True
        if reply in {'n', 'no', 'nop', 'nope'}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

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

  • 仅给出必需的参数:ask_ok('你真的想退出吗?')

  • 给出一个可选参数:ask_ok('是否覆盖该文件?', 2)

  • 或者甚至给出所有参数:ask_ok('是否覆盖该文件?', 2, '来吧,只能是是或否!')

此示例还介绍了 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.9.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 argument 'a'

当存在 **name 形式的最后一个形式参数时,它会接收一个字典 (请参阅 映射类型 — 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.9.3. 特殊参数

默认情况下,参数可以通过位置或显式通过关键字传递给 Python 函数。为了提高可读性和性能,限制参数的传递方式是有意义的,这样开发人员只需查看函数定义就可以确定项是通过位置、位置或关键字还是通过关键字传递的。

函数定义可能如下所示

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

其中 /* 是可选的。如果使用这些符号,则表示参数类型以及参数如何传递给函数:仅限位置、位置或关键字以及仅限关键字。关键字参数也称为命名参数。

4.9.3.1. 位置或关键字参数

如果函数定义中不存在 /*,则可以通过位置或关键字将参数传递给函数。

4.9.3.2. 仅限位置的参数

更详细地来看,可以将某些参数标记为仅限位置。如果为仅限位置,则参数的顺序很重要,并且参数不能通过关键字传递。仅限位置的参数放在 /(正斜杠)之前。/ 用于从逻辑上将仅限位置的参数与其他参数分开。如果函数定义中没有 /,则没有仅限位置的参数。

跟随 / 的参数可能是位置或关键字仅限关键字

4.9.3.3. 仅限关键字的参数

要将参数标记为仅限关键字,表示必须通过关键字参数传递参数,请在参数列表中的第一个仅限关键字参数之前放置一个 *

4.9.3.4. 函数示例

考虑以下示例函数定义,请密切注意标记 /*

>>> def standard_arg(arg):
...     print(arg)
...
>>> def pos_only_arg(arg, /):
...     print(arg)
...
>>> def kwd_only_arg(*, arg):
...     print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)

第一个函数定义 standard_arg,是最熟悉的形式,对调用约定没有限制,参数可以通过位置或关键字传递

>>> standard_arg(2)
2

>>> standard_arg(arg=2)
2

第二个函数 pos_only_arg 被限制为仅使用位置参数,因为函数定义中有一个 /

>>> pos_only_arg(1)
1

>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

第三个函数 kwd_only_arg 只允许关键字参数,如函数定义中的 * 所指示。

>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

>>> kwd_only_arg(arg=3)
3

最后一个函数在同一个函数定义中使用了所有三种调用约定。

>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

>>> combined_example(1, 2, kwd_only=3)
1 2 3

>>> combined_example(1, standard=2, kwd_only=3)
1 2 3

>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

最后,考虑这个函数定义,它在位置参数 name**kwds 之间存在潜在的冲突,后者将 name 作为键。

def foo(name, **kwds):
    return 'name' in kwds

没有任何可能的调用会使其返回 True,因为关键字 'name' 将始终绑定到第一个参数。例如:

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>

但是,使用 /(仅限位置参数),这是可能的,因为它允许 name 作为位置参数,并且允许 'name' 作为关键字参数中的键。

>>> def foo(name, /, **kwds):
...     return 'name' in kwds
...
>>> foo(1, **{'name': 2})
True

换句话说,仅限位置参数的名称可以在 **kwds 中使用,而不会产生歧义。

4.9.3.5. 总结

用例将决定在函数定义中使用哪些参数。

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

作为指导:

  • 如果您希望参数的名称对用户不可用,请使用仅限位置参数。当参数名称没有实际意义,或者您希望强制函数被调用时参数的顺序,或者您需要接受一些位置参数和任意关键字时,这很有用。

  • 当名称有意义,并且通过显式使用名称使函数定义更易于理解,或者您希望防止用户依赖于传递参数的位置时,请使用仅限关键字参数。

  • 对于 API,如果将来修改参数的名称,请使用仅限位置参数以防止 API 发生重大更改。

4.9.4. 任意参数列表

最后,最不常用的选项是指定可以使用任意数量的参数调用函数。这些参数将包装在一个元组中(参见 元组和序列)。在可变数量的参数之前,可以出现零个或多个普通参数。

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

当参数已经在列表或元组中,但需要为需要单独位置参数的函数调用解包时,会出现相反的情况。例如,内置的 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.9.6. 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.9.7. 文档字符串

以下是关于文档字符串的内容和格式的一些约定。

第一行应该始终是对象用途的简短、简洁的摘要。为了简洁起见,它不应明确说明对象的名称或类型,因为这些可以通过其他方式获得(除非名称恰好是一个描述函数操作的动词)。这一行应以大写字母开头,并以句点结尾。

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

Python 解析器不会从 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.9.8. 函数注解

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

现在您即将编写更长、更复杂的 Python 代码,现在是讨论编码风格的好时机。大多数语言都可以用不同的风格编写(或者更简洁地说,格式化);有些比其他的更具可读性。让其他人易于阅读您的代码始终是一个好主意,而采用一种良好的编码风格对此非常有帮助。

对于 Python,PEP 8 已成为大多数项目遵循的样式指南;它提倡一种非常易读且令人赏心悦目的编码风格。每位 Python 开发人员都应该在某个时候阅读它;以下是为您提取的最重要要点:

  • 使用 4 个空格缩进,而不是制表符。

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

  • 换行,使它们不超过 79 个字符。

    这有助于使用小显示器的用户,并使在较大的显示器上并排显示多个代码文件成为可能。

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

  • 在可能的情况下,将注释放在单独的一行上。

  • 使用文档字符串。

  • 在运算符周围和逗号之后使用空格,但不要直接在括号结构内部使用空格: a = f(1, 2) + g(3, 4)

  • 一致地命名您的类和函数;约定是对类使用 UpperCamelCase,对函数和方法使用 lowercase_with_underscores。始终使用 self 作为第一个方法参数的名称(有关类和方法的更多信息,请参阅初识类)。

  • 如果您的代码要在国际环境中使用,请不要使用花哨的编码。在任何情况下,Python 的默认 UTF-8 甚至纯 ASCII 效果最佳。

  • 同样,如果只有极小的机会让说不同语言的人阅读或维护代码,请不要在标识符中使用非 ASCII 字符。

脚注