在Python中,复合语句是一种包含其他语句或语句组的语句,它们以某种方式影响或控制所包含的其他语句的执行。通常情况下,复合语句跨越多行,并且使用缩进来表示语句之间的逻辑关系。在某些简单的情况下,整个复合语句也可能包含于一行之内。
Python中的复合语句包括条件语句(if、elif、else)、循环语句(while、for)、函数定义语句、类定义语句等等。这些复合语句通常由关键字、表达式、语句块等构成,并且可以根据需要嵌套使用。
在Python中,复合语句由一个或多个子句组成,每个子句包含一个句头和一个句体,特定复合语句的子句头都处于相同的缩进层级。每个子句头以一个作为唯一标识的关键字开始,并以一个冒号结束。子句体是由一个子句控制的一组语句。子句体可以是与子句头在同一行的一条或多条简单语句,也可以是在其之后缩进的一行或多行语句。只有后一种形式的子句体才能包含嵌套的复合语句。
以下形式是不合法的,这主要是因为无法分清某个后续的 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 | match_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 语句必须缩进来解决的)。
为了保证清晰,以下各节中语法规则采用将每个子句都放在单独行中的格式。
一、if语句
if 语句用于有条件的执行:
if_stmt ::= "if" assignment_expression ":" suite ("elif" assignment_expression ":" suite)* ["else" ":" suite]
它通过对表达式逐个求值直至找到一个真值在子句体中选择唯一匹配的一个;然后执行该子句体(而且 if 语句的其他部分不会被执行或求值)。 如果所有表达式均为假值,则如果 else 子句体如果存在就会被执行。
二、while语句
while 语句用于在表达式保持为真的情况下重复地执行:
while_stmt ::= "while" assignment_expression ":" suite ["else" ":" suite]
这将重复地检验表达式,并且如果其值为真就执行第一个子句体;如果表达式值为假(这可能在第一次检验时就发生)则如果 else 子句体存在就会被执行并终止循环。
第一个子句体中的 break 语句在执行时将终止循环且不执行 else 子句体。 第一个子句体中的 continue 语句在执行时将跳过子句体中的剩余部分并返回检验表达式。
三、for语句
for 语句用于对序列(例如字符串、元组或列表)或其他可迭代对象中的元素进行迭代:
for_stmt ::= "for" target_list "in" starred_list ":" suite ["else" ":" suite]
starred_list 表达式会被求值一次,它应当产生一个 iterable 对象,将针对该可迭代对象创建一个 iterator,随后该迭代器所提供的第一个条目将使用标准的赋值规则被赋值给目标列表 ,而代码块将被执行, 此过程将针对该迭代器所提供每个条目重复进行。 当迭代器被耗尽时,如果存在 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() 代表由整数组成的不可变算数序列。 例如,迭代 range(3) 将依次产生 0, 1 和 2。
四、try语句
参见《Python try语句》。
五、with语句
with 语句用于包装带有使用上下文管理器 定义的方法的代码块的执行,允许对普通的 try…except…finally 使用模式进行封装以方便地重用。
with_stmt ::= "with" ( "(" with_stmt_contents ","? ")" | with_stmt_contents ) ":" suite with_stmt_contents ::= with_item ("," with_item)* with_item ::= expression ["as" target]
带有一个“项目”的 with 语句的执行过程如下:
- 对上下文表达式(在 with_item 中给出的表达式)进行求值来获得上下文管理器;
- 载入上下文管理器的 __enter__() 以便后续使用;
- 载入上下文管理器的 __exit__() 以便后续使用;
- 发起调用上下文管理器的 __enter__() 方法;
- 如果一个目标被包括在 with 语句中,则把它赋值为 __enter__() 的返回值;
备注:with 语句会保证如果 __enter__() 方法未发生错误地返回,则 __exit__() 将一定被调用。 因此,如果在对目标列表赋值期间发生错误,它将被当作在语句体内部发生的错误来处理。
执行语句体。
- 发起调用上下文管理器的 __exit__() 方法。 如果语句体的退出是由异常导致的,则其类型、值和回溯信息将被作为参数传递给 __exit__()。 否则的话,将提供三个 None 参数;
- 如果语句体的退出是由异常导致的,并且来自 __exit__() 方法的返回值为假,则该异常会被重新引发。 如果返回值为真,则该异常会被抑制,并会继续执行 with 语句之后的语句;
- 如果语句体由于异常以外的任何原因退出,则来自 __exit__() 的返回值会被忽略,并会在该类退出正常的发生位置继续执行。
以下代码:
with EXPRESSION as TARGET: SUITE
在语义上等价于:
manager = (EXPRESSION) enter = type(manager).__enter__ exit = type(manager).__exit__ value = enter(manager) hit_except = False try: TARGET = value SUITE except: hit_except = True if not exit(manager, *sys.exc_info()): raise finally: if not hit_except: exit(manager, None, None, None)
如果有多个项目,则会视作存在多个 with 语句嵌套来处理多个上下文管理器:
with A() as a, B() as b: SUITE
在语义上等价于:
with A() as a: with B() as b: SUITE
也可以用圆括号包围的多行形式的多项目上下文管理器。例如:
with ( A() as a, B() as b, ): SUITE
六、match语句
参见《Python match语句》。
七、函数定义
函数定义就是对用户自定义函数的定义:
funcdef ::= [decorators] "def" funcname [type_params] "(" [parameter_list] ")" ["->" expression] ":" suite decorators ::= decorator+ decorator ::= "@" assignment_expression NEWLINE parameter_list ::= defparameter ("," defparameter)* "," "/" ["," [parameter_list_no_posonly]] | parameter_list_no_posonly parameter_list_no_posonly ::= defparameter ("," defparameter)* ["," [parameter_list_starargs]] | parameter_list_starargs parameter_list_starargs ::= "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]] | "**" parameter [","] parameter ::= identifier [":" expression] defparameter ::= parameter ["=" expression] funcname ::= identifier
函数定义是一条可执行语句,它执行时会在当前局部命名空间中将函数名称绑定到一个函数对象(函数可执行代码的包装器),这个函数对象包含对当前全局命名空间的引用,作为函数被调用时所使用的全局命名空间。函数定义并不会执行函数体;只有当函数被调用时才会执行此操作。
一个函数定义可以被一个或多个 decorator 表达式所包装。 当函数被定义时将在包含该函数定义的作用域中对装饰器表达式求值,求值结果必须是一个可调用对象,它会以该函数对象作为唯一参数被发起调用。 其返回值将被绑定到函数名称而非函数对象。 多个装饰器会以嵌套方式被应用。 例如以下代码:
@f1(arg) @f2 def func(): pass
大致等价于
def func(): pass func = f1(arg)(f2(func))
不同之处在于原始函数并不会被临时绑定到名称 func。可以在函数名及其形参列表开头圆括号之间加方括号给出一个 类型形参 的列表,这将向静态类型检查器指明该函数是泛型函数, 在运行时,类型形参可以从函数的 __type_params__ 属性中提取。
当一个或多个形参具有形参 = 表达式这样的形式时,该函数就被称为具有“默认形参值”。 对于一个具有默认值的形参,其对应的 argument 可以在调用中被省略,在此情况下会用形参的默认值来替代。 如果一个形参具有默认值,后续所有在 “*” 之前的形参也必须具有默认值 — 这个句法限制并未在语法中明确表达。
默认形参值会在执行函数定义时按从左至右的顺序被求值,当函数被定义时将对表达式求值一次,相同的“预计算”值将在每次调用时被使用,这一点在默认形参为可变对象,例如列表或字典的时候尤其需要重点理解:如果函数修改了该对象(例如向列表添加了一项),则实际上默认值也会被修改,这通常不是大家想要的。 解决方法是使用 None 作为默认值,并在函数体中显式地对其进测试,例如:
def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of the zoo") return penguin
函数调用总是会给形参列表中列出的所有形参赋值,或是用位置参数,或是用关键字参数,或是用默认值。 如果存在 “*identifier” 这样的形式,它会被初始化为一个元组来接收任何额外的位置参数,默认为一个空元组; 如果存在 “**identifier” 这样的形式,它会被初始化为一个新的有序映射来接收任何额外的关键字参数,默认为一个相同类型的空映射; 在 “*” 或 “*identifier” 之后的形参都是仅限关键字形参因而只能通过关键字参数传入; 在 “/” 之前的形参都是仅限位置形参因而只能通过位置参数传入。
形参可以带有 标注,其形式为在形参名称后加上 “: expression”。 任何形参都可以带有标注,甚至 *identifier 或 **identifier 这样的形参也可以。 函数可以带有“返回”标注,其形式为在形参列表后加上 “-> expression”, 这些标注可以是任何有效的 Python 表达式, 而不会改变函数的语义。 标注值可以作为函数对象的 __annotations__ 属性中以对应形参名称为键的字典值被访问。 如果使用了 annotations import from __future__ 的方式,则标注会在运行时保存为字符串以启用延迟求值特性,否则它们会在执行函数定义时被求值。 在这种情况下,标注的求值顺序可能与它们在源代码中出现的顺序不同。
创建匿名函数(未绑定到一个名称的函数)以便立即在表达式中使用也是可能的,这就需要使用 lambda 表达式,请注意 lambda 只是简单函数定义的一种简化写法;在 “def” 语句中定义的函数也可以像用 lambda 表达式定义的函数一样被传递或赋值给其他名称。 “def” 形式实际上更为强大,因为它允许执行多条语句和使用标注。
八、类定义
类定义就是对类对象的定义:
classdef ::= [decorators] "class" classname [type_params] [inheritance] ":" suite inheritance ::= "(" [argument_list] ")" classname ::= identifier
类定义是一条可执行语句,其中继承列表通常给出基类的列表,列表中的每一项都应当被求值为一个允许子类的类对象。 没有继承列表的类默认继承自基类 object;因此:
class Foo: pass
等价于
class Foo(object): pass
随后类体将在一个新的执行帧 中被执行,使用新创建的局部命名空间和原有的全局命名空间。当类体结束执行时,其执行帧将被丢弃而其局部命名空间会被保存; 一个类对象随后会被创建,其基类使用给定的继承列表,属性字典使用保存的局部命名空间; 类名称将在原有的全局命名空间中绑定到该类对象。
在类体内定义的属性的顺序保存在新类的 __dict__ 中。 请注意此顺序的可靠性只限于类刚被创建时,并且只适用于使用定义语法所定义的类。
类的创建可使用元类进行重度定制。类也可以被装饰:就像装饰函数一样:
@f1(arg) @f2 class Foo: pass
大致等价于:
class Foo: pass Foo = f1(arg)(f2(Foo))
装饰器表达式的求值规则与函数装饰器相同。 结果随后会被绑定到类名称。
可以在类名之后的方括号中列出 类型形参。 这将向静态类型检查器指明该类是泛型类。 在运行时,可以从类的 __type_params__ 属性中获取类型参数。 请参阅 泛型类 了解详情。
程序员注意事项:在类定义内定义的变量是类属性;它们将被类实例所共享。 实例属性可通过 self.name = value 在方法中设定。 类和实例属性均可通过 “self.name” 表示法来访问,当通过此方式访问时实例属性会隐藏同名的类属性。 类属性可被用作实例属性的默认值,但在此场景下使用可变值可能导致未预期的结果。 可以使用 描述器 来创建具有不同实现细节的实例变量。
九、协程
参见《Python协程》。
十、类型形参列表
参见《Python类型形参列表》。