7. 复合语句

优质
小牛编辑
129浏览
2023-12-01

7. 复合语句

复合语句包含(多组)其它语句;它们以某种方式影响或者控制其它那些语句的执行。通常,复合语句跨越多行,虽然一条完整的复合语句可以用简洁的形式包含在一行之中。

if、while和for语句实现传统的控制流句法结构。try指出一组语句的异常处理器和/或清理代码。函数和类定义在语法上同样也是复合语句。

复合语句由一个或多个‘子句’组成。一条子句由语句首和‘语句组’组成。一条特定的复合语句的所有子句的语句首都处在相同的缩进水平上。每一个子句的语句首以一个唯一的标识关键字开始并以冒号结束。语句组是由一条子句控制的一组语句。一个语句组可以是语句首冒号之后的同一行上紧跟一个或多个分号分隔的简单语句,也可以是后续行上一个或多个缩进的语句。只有后一种形式的语句组可以包含嵌套的复合语句;下面的语句是非法的,最主要是因为不能明确随后的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
                   | decorated
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语句必须缩进得到解决)。

为了清晰起见,下面小节中的语法规则的格式会将子句放在单独的一行。

7.1. if 语句

if 语句用于条件执行:

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

它通过对表达式逐个求值直到其中一个为真的方式准确地选择一个语句组(真和假的定义参见布尔操作 一节);然后执行这个语句组(if语句的其它部分不会被执行或求值)。如果所有表达式都为假,则执行else子句的语句组。

7.2. while 语句

while语句用于重复执行直到某个表达式为真:

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

它重复测试表达式,如果为真,则执行第一个语句组;如果表达式为假(可能是第一次测试),则执行else子句且终止循环。

第一个语句组中执行的break语句会终止循环而不执行else子句的语句组。在第一个语句组中执行的continue语句会跳过语句组中剩余的语句并返回继续测试表达式。

7.3. for 语句

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

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

表达式列表值计算一次;它应当产生一个可迭代的对象。expression_list的结果创建一个迭代器。然后以索引的升序为顺序,为迭代器提供的每个元素执行一次语句组。每个元素使用标准的赋值规则被赋值给目标列表,然后执行语句组。当元素取尽时(如果序列为空则立刻取尽),则执行else子句中的语句组并终止循环。

第一个语句组中的break语句会终止循环而不执行else子句的语句组。在第一个语句组中执行的continue语句会跳过语句组的剩余语句并继续下一个元素,如果没有下一个元素则继续执行else子句。

语句组可以给目标列表中的变量赋值;这不影响下一个赋值给它的元素。

当循环结束时目标序列不会被删除,但是如果序列为空,循环不会赋任何值给它。提示:内建的range()函数返回整数的序列,它适用于模拟Pascal 语言中fori:=atobdo效果;例如,range(3) 返回列表[0,1,2]。

注意

当序列被循环修改时,会发生微妙的事情(只有可变类型的序列会发生,例如列表)。有一个内部计数器用于跟踪下一轮循环使用哪一个元素,并且每次迭代中会增加。当计数器达到序列的长度时循环终止。这意味着如果语句组从序列中删除当前(或者前面的)元素,下一个元素将被跳过(因为它获取当前已经被处理过的元素的索引)。同样地,如果语句组在序列中当前元素之前插入一个元素,那么当前元素将在下一次循环被再次处理。这可能导致难以觉察的错误,但可以通过使用整个序列的切片生成临时拷贝避免这个问题,例如,

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

7.4. try 语句

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

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

版本2.5 中的新变化:在以前版本的Python 中,try...except...finally 不工作。try...except必须嵌套在try...finally中。

except子句指定一个或多个异常处理器.。当try子句中没有出现异常时,不会执行异常处理器。当try语句组中出现异常时,开始搜索异常处理器。搜索依次检查异常子句直到找到与异常匹配的一个。没有表达式的异常子句,如果出现,必须放在最后;它匹配任何异常。对于一个带有表达式的异常子句,该表达式将被求值,如果结果对象与异常“兼容”,则认为子句与异常匹配。对象与异常兼容,如果它是异常对象的类或基类或者是一个包含与异常兼容的元素的元组。

如果没有except子句匹配到异常,异常处理器的搜索将继续在外层代码和调用栈上进行。[1]

如果计算except子句头部的一个表达式引发了异常, 那么就会中断原异常处理器的搜索, 而在外层代码和调用栈上搜索新的异常处理器(就好像是整个try语句发生了异常一样)。

当找到一个匹配的except子句时,异常就被赋给excep子句中的目标,然后执行excep子句的语句组。所有的异常子句必须具有一个可执行的代码块。当到达该代码块的结尾时,在真个try语句之后执行正常继续。(这意味着如果同一个异常存在两个嵌套的处理器,且异常发生在内部处理器的try子句中,那么外边的处理器不会处理这个异常。)

在执行except子句的语句组之前,异常的详细信息被赋值给sys模块中的三个变量:sys.exc_type接收标识异常的对象;sys.exc_value接收异常的参数;sys.exc_traceback接收一个回溯对象(参见标准类型的层级一节)指示程序中异常发生的点。这些详细信息也可以通过sys.exc_info()函数得到,它返回一个元组(exc_type,exc_value,exc_traceback)。鼓励使用这个函数而不鼓励使用对应的变量,因为在多线程程序中它们的使用是不安全的。从Python 1.5 开始,这些值会在处理异常的函数返回时会恢复它们之前的值(调用之前的值)。

如果控制流从try子句的结尾出来时,则执行可选的else子句。[2]else子句中的异常不会被前面的except子句处理。

如果有finally出现,它指定一个“清除”处理器。首先执行try子句被执行,然后包括任何except和else子句。如果异常发生在任何子句中且没有被处理,那么异常会被临时保存起来。最后执行finally子句。如果有保存的异常,它会在finally子句结束时被重新抛出。如果finally抛出另外一个异常或者执行一个return或break语句,那么保存的异常会被丢弃:

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

在finally子句执行过程中程序访问不到异常信息。

当return、break或continue语句在try...finally语句的try语句组中被执行,finally子句在‘出口’处同样被执行。continue语句出现在finally子句中是非法的。(原因是当前实现的问题 — 该限制在未来可能会去掉)。

函数的返回值取决于执行的最后一条return语句。因为finally子句会永远执行,在finally子句中执行的return语句将永远是最后执行的一条语句:

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

额外的信息可以在异常一节中找到,关于如何使用raise语句产生异常可以在raise语句一节中找到。

7.5. with 语句

出现于版本2.5。

with用于和上下文管理器定义的方法一起封装代码块的执行(参见With语句的上下文管理器一节)。这允许常见的try...except...finally的用法模式封装起来以方便地重用。

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

带有一个“item”的with语句的执行按下面的方式进行:

  1. 计算上下文表达式(with_item中给出的表达式)以获得一个上下文管理器。

  2. 加载上下文管理器的exit()方法留着后面使用。

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

  4. 如果with语句包含一个目标,enter()的返回值将赋值给它。

注意

with语句保证如果enter()方法没有错误返回,那么exit()将永远被调用。因此,如果在给目标列表赋值过程中出现错误,它将被与语句组中发生的错误同样对待。参见下面的第6步。

  1. 执行语句组。

  2. 调用上下文管理器的exit()方法。如果异常导致语句组要退出,那么异常的类型、值和回溯栈被当做参数传递给exit()。否则,提供三个None 参数。

如果语句组由于异常退出,且exit()方法的返回值为假,异常将被重新引发。如果返回值为真,异常将被取消,并继续执行with语句之后的语句。

如果语句组由于异常以外的其它任何原因退出,exit()的返回值将被忽略,执行将在退出发生的正常位置继续。

如果有多个条目,上下文管理器的处理如同嵌套的多个with语句:

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

等同于

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

注意

在Python 2.5中,with只有在with_statement特性被启用的时候才允许使用。在Python 2.6中,它被永远启用。

版本2.7 中的新变化:支持多个上下文表达式。

另请参阅

PEP 0343 - “with”语句Python with语句的说明、背景和实例。

7.6. 函数定义

函数定义定义一个用户自定义的函数对象(参见标准类型的层次一节):

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

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

函数定义不会执行函数体;它只有在调用函数的时候才执行。[3]

函数定义可能被一个或多个修饰符表达式封装。修饰符表达式在函数定义时于包含函数定义的定义域中求值。求值的结果必须是一个可调用对象,它以该函数对象为唯一的参数。调用的返回值绑定在函数名而不是函数对象上。多个修饰符是以嵌套的方式作用的。例如,下面的代码:

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

等同于:

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

当一个或多个最上层的参数具有parameter=expression的形式时,称该函数具有“默认参数值。”对于具有默认值的参数,对应的参数在调用时可以省略,在这种情况下使用参数的默认值。如果一个参数具有默认值,所有随后的参数也必须具有默认值 — 这个限制在语法中没有表达出来的。

默认的参数值在执行函数定义是求值。这意味着只在函数定义的时候该表达式求一次值,以后每次调用使用相同的“提前计算好的”值。这对于理解默认参数是可变对象时特别重要,例如列表或字典:如果函数修改了该对象(例如,向列表添加一个元素),默认值将受影响被修改。这通常不是想要的。一种变通的方法是使用None作为默认值,然后在函数体中明确地测试它,例如:

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

函数调用的语义在调用一节有更详细的描述。函数调用永远会给参数列表中的所有的参数赋值,无论是位置参数还是关键字参数或默认值。如果出现“identifier”的形式,那么它被初始化为一个可以接收任意多余位置参数元组,默认为空元组。如果有“*identifier”的形式,那么它被初识化为一个可以接收任意的多余关键字参数的新的字典,默认值为空字典。

也可以创建匿名函数(没有绑定到某个名称的函数),以在表达式中直接使用。它使用lambda 表达式,在Lambdas一节中有详细描述。注意lambda 表达式仅仅是简单的函数定义的简写;“def”语句中定义的函数和lambda 表达式定义的函数一样,可以传递或者赋值给另外一个名称。“def” 形式事实上功能更强大,因为它允许执行多条语句。

程序员的注意事项:函数是第一级的对象。在函数内部执行的“def”形式定义一个可以被返回或传递的局部函数。在嵌套的函数中使用的自由变量可以访问包含该def的函数的局部变量。详细信息参见名称和绑定一节。

7.7. 类定义

类定义定义一个类对象(参见标准类型的层次 一节):

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

类定义是一个可执行语句。它首先计算inheritance序列,如果存在的话。inheritance序列中的每一项都应该是一个类对象或者允许生成子类的类类型。然后使用一个新创建的局部命名空间和初始的全局命名空间,在新的执行帧中执行类的语句组(参见名称和绑定一节)。(通常,语句组只包含函数的定义。)当类的语句组结束执行,它的执行帧被丢弃但是局部命名空间被保存下来。[4]最后使用inheritance序列作为基类创建一个类对象,并保存局部命名空间作为属性字典。类的名称被绑定到初识局部命名空间中类对象。

程序员的注意事项:类定义中定义的变量是类变量;它们被所有的实例共享。要创建实例变量,可以在方法中使用self.name=value设置它们。类变量和实例变量都可以通过“self.name”记号访问,当用相同的方式访问时实例变量将隐藏相同名称的类变量。类变量可以作为实例变量的默认值使用,但是这种用法如果使用可变的值可能导致意想不到的结果。对于新式类,可以使用描述符创建具有不同实现细节的实例变量。

类定义,类似于函数定义,可以被一个或多个描述符 表达式封装。描述符表达式的计算规则和函数相同。结果必须是一个类对象,并绑定于类的名字。

脚注

[1]异常将扩散到调用栈除非finally子句碰巧引发另外一个异常。这个新的异常导致旧的异常丢失。
[2]目前,控制“从末尾流出”除了下面这些情况:异常或执行return、continue、break语句。
[3]作为函数体第一条语句出现的字符串字面值被转换成函数的doc属性,即函数的文档字符串

| [4] | 作为类体的第一条语句出现的语句被转换为该命名空间的doc属性,即类的文档字符串。 | |-----|-----|