8. 复合语句 — Python 文档

来自菜鸟教程
Python/docs/3.7/reference/compound stmts
跳转至:导航、​搜索

8. 复合语句

复合语句包含(组)其他语句; 它们以某种方式影响或控制其他语句的执行。 通常,复合语句跨越多行,尽管在简单的化身中,整个复合语句可能包含在一行中。

ifwhilefor 语句实现了传统的控制流结构。 try 为一组语句指定异常处理程序和/或清理代码,而 with 语句允许围绕代码块执行初始化和终止代码。 函数和类定义也是语法上的复合语句。

复合语句由一个或多个“子句”组成。 子句由标题和“套件”组成。 特定复合语句的子句标题都处于相同的缩进级别。 每个子句标题都以唯一标识关键字开始,并以冒号结束。 套件是由子句控制的一组语句。 一个套件可以是一个或多个分号分隔的简单语句,与标题位于同一行,跟随标题的冒号,或者它可以是后续行中的一个或多个缩进语句。 只有后一种形式的套件才能包含嵌套的复合语句; 以下是非法的,主要是因为不清楚 else 子句属于哪个 if 子句:

if test1: if test2: print(x)

另请注意,在此上下文中分号比冒号绑定得更紧密,因此在以下示例中,会执行所有或不执行 print() 调用:

if x < y < z: print(x); print(y); print(z)

总结:

compound_stmt ::=  if_stmt
                   | while_stmt
                   | for_stmt
                   | try_stmt
                   | with_stmt
                   | funcdef
                   | classdef
                   | async_with_stmt
                   | async_for_stmt
                   | async_funcdef
suite         ::=  stmt_list NEWLINE | NEWLINE INDENT statement+ DEDENT
statement     ::=  stmt_list NEWLINE | compound_stmt
stmt_list     ::=  simple_stmt (";" simple_stmt)* [";"]

请注意,语句总是以 NEWLINE 结尾,可能后跟 DEDENT。 另请注意,可选的延续子句始终以不能启动语句的关键字开头,因此没有歧义(“悬空 else”问题在 Python 中通过要求嵌套的 if 语句解决缩进)。

为清楚起见,以下各节中语法规则的格式将每个子句放在单独的行中。

8.1. 这if陈述

if 语句用于条件执行:

if_stmt ::=  "if" expression ":" suite
             ("elif" expression ":" suite)*
             ["else" ":" suite]

它通过对表达式一一求值直到发现其中一个为真来精确地选择其中一个套件(有关真假的定义,请参见布尔运算部分); 然后执行该套件(并且不执行或评估 if 语句的其他部分)。 如果所有表达式都为假,则执行 else 子句套件(如果存在)。


8.2. 这while陈述

while 语句用于重复执行,只要表达式为真:

while_stmt ::=  "while" expression ":" suite
                ["else" ":" suite]

这会反复测试表达式,如果为真,则执行第一个套件; 如果表达式为假(这可能是第一次测试),则执行 else 子句套件(如果存在)并终止循环。

在第一个套件中执行的 break 语句终止循环而不执行 else 子句的套件。 在第一个套件中执行的 continue 语句跳过套件的其余部分并返回测试表达式。


8.3. 这for陈述

for 语句用于迭代序列(例如字符串、元组或列表)或其他可迭代对象的元素:

for_stmt ::=  "for" target_list "in" expression_list ":" suite
              ["else" ":" suite]

表达式列表被评估一次; 它应该产生一个可迭代的对象。 为 expression_list 的结果创建迭代器。 然后,按照迭代器返回的顺序,对迭代器提供的每个项目执行一次套件。 使用标准分配规则(参见 分配语句 )依次将每个项目分配给目标列表,然后执行套件。 当项目用完时(即当序列为空或迭代器引发 StopIteration 异常时),执行 else 子句中的套件(如果存在),并执行循环终止。

在第一个套件中执行的 break 语句终止循环而不执行 else 子句的套件。 在第一个套件中执行的 continue 语句跳过套件的其余部分并继续下一项,如果没有下一项,则使用 else 子句。

for 循环对目标列表中的变量进行赋值。 这将覆盖所有先前对这些变量的分配,包括在 for 循环套件中进行的分配:

for i in range(10):
    print(i)
    i = 5             # this will not affect the for-loop
                      # because i will be overwritten with the next
                      # index in the range

循环结束时,目标列表中的名称不会被删除,但如果序列为空,则循环根本不会将它们分配给它们。 提示:内置函数 range() 返回一个整数迭代器,适合模拟 Pascal 的 for i := a to b do 的效果; 例如,list(range(3)) 返回列表 [0, 1, 2]

笔记

当循环修改序列时有一个微妙之处(这只能发生在可变序列,例如 列表)。 内部计数器用于跟踪下一个使用哪个项目,并且在每次迭代时递增。 当这个计数器达到序列的长度时,循环终止。 这意味着如果套件从序列中删除当前(或前一个)项目,则将跳过下一个项目(因为它获取已处理的当前项目的索引)。 同样,如果套件在当前项目之前的序列中插入一个项目,则当前项目将在下一次循环中再次被处理。 这可能会导致令人讨厌的错误,可以通过使用整个序列的一部分制作临时副本来避免这些错误,例如,

for x in a[:]:
    if x < 0: a.remove(x)

8.4. 这try陈述

try 语句为一组语句指定异常处理程序和/或清理代码:

try_stmt  ::=  try1_stmt | try2_stmt
try1_stmt ::=  "try" ":" suite
               ("except" [expression ["as" identifier]] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try2_stmt ::=  "try" ":" suite
               "finally" ":" suite

except 子句指定一个或多个异常处理程序。 当 try 子句中没有异常发生时,不执行异常处理程序。 当 try 套件中发生异常时,将开始搜索异常处理程序。 此搜索依次检查 except 子句,直到找到与异常匹配的子句。 无表达式的 except 子句,如果存在,必须放在最后; 它匹配任何异常。 对于带有表达式的 except 子句,该表达式被计算,如果结果对象与异常“兼容”,则该子句匹配异常。 如果对象是异常对象的类或基类,或者包含与异常兼容的项目的元组,则该对象与异常兼容。

如果没有except 子句与异常匹配,则在周围代码和调用堆栈中继续搜索异常处理程序。 1

如果对 except 子句头中的表达式求值引发异常,则取消对处理程序的原始搜索,并开始在周围代码和调用堆栈中搜索新异常(它被视为整个try 语句引发了异常)。

当找到匹配的 except 子句时,将异常分配给在该 except 子句中的 as 关键字之后指定的目标(如果存在),并且执行 except 子句的套件。 所有except子句都必须有一个可执行块。 当到达该块的末尾时,在整个 try 语句之后继续正常执行。 (这意味着如果对于同一个异常存在两个嵌套的处理程序,并且异常发生在内部处理程序的 try 子句中,外部处理程序将不会处理该异常。)

当使用 as target 分配异常时,它会在 except 子句的末尾清除。 这好像

except E as N:
    foo

被翻译成

except E as N:
    try:
        foo
    finally:
        del N

这意味着必须将异常分配给不同的名称,以便能够在 except 子句之后引用它。 异常被清除是因为通过附加到它们的回溯,它们与堆栈帧形成一个引用循环,使该帧中的所有局部变量保持活动状态,直到下一次垃圾回收发生。

在执行 except 子句的套件之前,有关异常的详细信息存储在 sys 模块中,并可通过 sys.exc_info() 访问。 sys.exc_info() 返回一个由异常类、异常实例和回溯对象组成的三元组(参见 标准类型层次结构 部分),标识程序中的点发生异常。 当从处理异常的函数返回时,sys.exc_info() 值将恢复到它们以前的值(在调用之前)。

如果控制流离开 try 套件,没有引发异常,并且没有 returncontinuebreak 语句被执行。 else 子句中的异常不会由前面的 except 子句处理。

如果 finally 存在,它指定一个“清理”处理程序。 执行 try 子句,包括任何 exceptelse 子句。 如果在任何一个子句中发生异常并且没有被处理,则该异常被临时保存。 finally 子句被执行。 如果有保存的异常,它会在 finally 子句的末尾重新引发。 如果 finally 子句引发另一个异常,则将保存的异常设置为新异常的上下文。 如果 finally 子句执行 returnbreak 语句,则丢弃保存的异常:

>>> def f():
...     try:
...         1/0
...     finally:
...         return 42
...
>>> f()
42

在执行 finally 子句期间,程序无法获得异常信息。

returnbreakcontinue 语句在 tryfinally 语句中,finally 子句也执行了“出路”。 continue 语句在 finally 子句中是非法的。 (原因是目前的实现有问题——这个限制可能会在未来取消)。

函数的返回值由最后执行的 return 语句决定。 由于 finally 子句始终执行,因此在 finally 子句中执行的 return 语句将始终是最后执行的:

>>> def foo():
...     try:
...         return 'try'
...     finally:
...         return 'finally'
...
>>> foo()
'finally'

关于异常的更多信息可以在 Exceptions 部分找到,关于使用 raise 语句生成异常的信息可以在 raise 语句 部分找到。


8.5. 这with陈述

with 语句用于用上下文管理器定义的方法包装块的执行(参见 With 语句上下文管理器 部分)。 这允许封装常见的 try...except...finally 使用模式,以便于重用。

with_stmt ::=  "with" with_item ("," with_item)* ":" suite
with_item ::=  expression ["as" target]

带有一个“项目”的 with 语句的执行过程如下:

  1. 对上下文表达式(with_item 中给出的表达式)求值以获得上下文管理器。

  2. 上下文管理器的 __exit__() 被加载以备后用。

  3. 上下文管理器的 __enter__() 方法被调用。

  4. 如果目标包含在 with 语句中,则将 __enter__() 的返回值分配给它。

    笔记

    with 语句保证如果 __enter__() 方法返回没有错误,那么 __exit__() 将始终被调用。 因此,如果在分配给目标列表的过程中发生错误,它将被视为套件中发生的错误。 请参阅下面的步骤 6。

  5. 套房被执行。

  6. 上下文管理器的 __exit__() 方法被调用。 如果异常导致套件退出,则其类型、值和回溯将作为参数传递给 __exit__()。 否则,提供三个 None 参数。

    如果套件因异常退出,并且 __exit__() 方法的返回值为 false,则重新引发异常。 如果返回值为真,则异常被抑制,并继续执行 with 语句之后的语句。

    如果套件因异常以外的任何原因退出,则忽略来自 __exit__() 的返回值,并在所采取的退出类型的正常位置继续执行。

对于不止一项,上下文管理器的处理就像嵌套了多个 with 语句:

with A() as a, B() as b:
    suite

相当于

with A() as a:
    with B() as b:
        suite

3.1 版更改: 支持多上下文表达式。


也可以看看

PEP 343 - “with”语句
Python with 语句的规范、背景和示例。


8.6. 功能定义

一个函数定义定义了一个用户定义的函数对象(参见 标准类型层次结构 部分):

funcdef                 ::=  [decorators] "def" funcname "(" [parameter_list] ")"
                             ["->" expression] ":" suite
decorators              ::=  decorator+
decorator               ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE
dotted_name             ::=  identifier ("." identifier)*
parameter_list          ::=  defparameter ("," defparameter)* ["," [parameter_list_starargs]]
                             | parameter_list_starargs
parameter_list_starargs ::=  "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]]
                             | "**" parameter [","]
parameter               ::=  identifier [":" expression]
defparameter            ::=  parameter ["=" expression]
funcname                ::=  identifier

函数定义是一个可执行语句。 它的执行将当前本地命名空间中的函数名称绑定到一个函数对象(函数的可执行代码的包装器)。 该函数对象包含对当前全局命名空间的引用,作为调用该函数时要使用的全局命名空间。

函数定义不执行函数体; this 仅在调用函数时执行。 2

一个函数定义可以被一个或多个 decorator 表达式包裹。 在定义函数时,在包含函数定义的范围内评估装饰器表达式。 结果必须是可调用的,以函数对象作为唯一参数调用。 返回值绑定到函数名而不是函数对象。 多个装饰器以嵌套方式应用。 例如,下面的代码

@f1(arg)
@f2
def func(): pass

大致相当于

def func(): pass
func = f1(arg)(f2(func))

除了原来的函数暂时没有绑定到名称func

当一个或多个 参数 具有 参数 = 表达式 的形式时,该函数被称为具有“默认参数值”。 对于具有默认值的参数,可以从调用中省略相应的 参数 ,在这种情况下,参数的默认值将被替换。 如果参数具有默认值,则直到“*”之前的所有后续参数也必须具有默认值——这是语法未表达的句法限制。

执行函数定义时从左到右计算默认参数值。每次通话。 这对于理解默认参数何时是可变对象(例如列表或字典)尤其重要:如果函数修改了对象(例如 通过将项目附加到列表),实际上修改了默认值。 这通常不是预期的。 解决这个问题的一种方法是使用 None 作为默认值,并在函数体中明确测试它,例如:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

函数调用语义在 Calls 部分有更详细的描述。 函数调用始终为参数列表中提到的所有参数赋值,无论是位置参数、关键字参数还是默认值。 如果存在“*identifier”形式,则将其初始化为接收任何多余位置参数的元组,默认为空元组。 如果存在“**identifier”形式,则它被初始化为一个新的有序映射,接收任何多余的关键字参数,默认为相同类型的新空映射。 “*”或“*identifier”之后的参数是仅关键字参数,只能传递使用过的关键字参数。

参数可能在参数名称后面有一个 注释 ,格式为“: expression”。 任何参数都可以有注释,即使是 *identifier**identifier 形式的参数。 函数可能在参数列表后具有“-> expression”形式的“返回”注释。 这些注释可以是任何有效的 Python 表达式。 注解的存在不会改变函数的语义。 注释值可用作由函数对象的 __annotations__ 属性中的参数名称键控的字典值。 如果使用从 __future__ 导入的 annotations,注释将在运行时保留为字符串,从而实现延迟评估。 否则,它们会在执行函数定义时进行评估。 在这种情况下,注释的计算顺序可能与它们在源代码中出现的顺序不同。

还可以创建匿名函数(未绑定到名称的函数),以便在表达式中立即使用。 这使用了 lambda 表达式,在 Lambdas 部分中进行了描述。 请注意,lambda 表达式只是简化函数定义的简写; 在“def”语句中定义的函数可以传递或分配给另一个名称,就像由 lambda 表达式定义的函数一样。 “def”形式实际上更强大,因为它允许执行多个语句和注释。

程序员注:函数是一等对象。 在函数定义中执行的“def”语句定义了一个可以返回或传递的局部函数。 嵌套函数中使用的自由变量可以访问包含 def 的函数的局部变量。 有关详细信息,请参阅 命名和绑定 部分。

也可以看看

PEP 3107 - 函数注解
函数注释的原始规范。
PEP 484 - 类型提示
注释的标准含义定义:类型提示。
PEP 526 - 变量注释的语法
能够键入提示变量声明,包括类变量和实例变量
PEP 563 - 注释的延迟评估
通过在运行时以字符串形式保留注释而不是预先求值来支持注释中的前向引用。


8.7. 类定义

类定义定义了一个类对象(参见 标准类型层次结构 部分):

classdef    ::=  [decorators] "class" classname [inheritance] ":" suite
inheritance ::=  "(" [argument_list] ")"
classname   ::=  identifier

类定义是一个可执行语句。 继承列表通常给出一个基类列表(更高级的用法参见 Metaclasses),所以列表中的每一项都应该评估为一个允许子类化的类对象。 没有继承列表的类默认从基类 object 继承; 因此,

class Foo:
    pass

相当于

class Foo(object):
    pass

然后使用新创建的本地命名空间和原始全局命名空间在新的执行框架中执行该类的套件(请参阅 命名和绑定 )。 (通常,套件主要包含函数定义。)当类的套件完成执行时,它的执行帧将被丢弃,但它的本地命名空间会被保存。 3 然后使用基类的继承列表和为属性字典保存的本地命名空间创建类对象。 类名绑定到原始本地命名空间中的这个类对象。

在类主体中定义属性的顺序保留在新类的 __dict__ 中。 请注意,这仅在创建类之后才可靠,并且仅适用于使用定义语法定义的类。

可以使用 元类 大量自定义类创建。

类也可以被装饰:就像装饰函数时一样,

@f1(arg)
@f2
class Foo: pass

大致相当于

class Foo: pass
Foo = f1(arg)(f2(Foo))

装饰器表达式的计算规则与函数装饰器相同。 然后将结果绑定到类名。

程序员注:类定义中定义的变量为类属性; 它们由实例共享。 实例属性可以在self.name = value的方法中设置。 类和实例属性都可以通过符号“self.name”访问,并且实例属性在以这种方式访问时隐藏了具有相同名称的类属性。 类属性可以用作实例属性的默认值,但在那里使用可变值可能会导致意外结果。 Descriptors 可用于创建具有不同实现细节的实例变量。

也可以看看

PEP 3115 - Python 3000 中的元类
将元类的声明更改为当前语法的提议,以及如何构造具有元类的类的语义。
PEP 3129 - 类装饰器
添加类装饰器的提案。 在 PEP 318 中引入了函数和方法装饰器。


8.8. 协程

3.5 版中的新功能。


8.8.1. 协程函数定义

async_funcdef ::=  [decorators] "async" "def" funcname "(" [parameter_list] ")"
                   ["->" expression] ":" suite

Python 协程的执行可以在许多点暂停和恢复(参见 coroutine)。 在协程函数体内,awaitasync 标识符成为保留关键字; await 表达式、async forasync with 只能在协程函数体中使用。

使用 async def 语法定义的函数始终是协程函数,即使它们不包含 awaitasync 关键字。

在协程函数体内使用 yield from 表达式是一个 SyntaxError

协程函数的一个例子:

async def func(param1, param2):
    do_stuff()
    await some_coroutine()

8.8.2. 这async for陈述

async_for_stmt ::=  "async" for_stmt

异步迭代器能够在其iter实现中调用异步代码,而异步迭代器可以在其next方法中调用异步代码。

async for 语句允许对异步迭代器进行方便的迭代。

以下代码:

async for TARGET in ITER:
    BLOCK
else:
    BLOCK2

在语义上等同于:

iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
    try:
        TARGET = await type(iter).__anext__(iter)
    except StopAsyncIteration:
        running = False
    else:
        BLOCK
else:
    BLOCK2

有关详细信息,另请参阅 __aiter__()__anext__()

在协程函数体外使用 async for 语句是一个 SyntaxError


8.8.3. 这async with陈述

async_with_stmt ::=  "async" with_stmt

异步上下文管理器 是一个 上下文管理器 ,它能够在其 enterexit 方法中暂停执行。

以下代码:

async with EXPR as VAR:
    BLOCK

在语义上等同于:

mgr = (EXPR)
aexit = type(mgr).__aexit__
aenter = type(mgr).__aenter__(mgr)

VAR = await aenter
try:
    BLOCK
except:
    if not await aexit(mgr, *sys.exc_info()):
        raise
else:
    await aexit(mgr, None, None, None)

有关详细信息,另请参阅 __aenter__()__aexit__()

在协程函数体外使用 async with 语句是一个 SyntaxError

也可以看看

PEP 492 - 具有异步和等待语法的协程
使协程成为 Python 中适当的独立概念的提议,并添加了支持语法。


脚注

1
除非有 finally 子句碰巧引发另一个异常,否则异常将传播到调用堆栈。 新的异常会导致旧的异常丢失。
2
作为函数体中第一条语句出现的字符串文字被转换为函数的 __doc__ 属性,因此是函数的 docstring
3
作为类主体中第一条语句出现的字符串文字被转换为命名空间的 __doc__ 项,因此转换为类的 docstring