8. 复合语句
复合语句指的是那种包含了其他语句, 也可以说是语句组在内的语句;它的主要作用是以某种特定的方式来影响或者去控制在内部所包裹的那些其他语句究竟是怎样去执行下去的。
在通常的一般情况之下, 这种复合语句往往会跨越好几行才能展示完整, 虽然在有的某些较为简单的形式里面, 整个一个复合语句也有可能全部被压缩塞进同一行以内去完成表达。
语句和语句用来实现传统的控制流程构造。 语句为一组语句指定异常处理和/或者清理代码, 而语句允许在一个代码块周围执行初始化和终结化代码。 函数和类定义在语法上也属于复合语句。
一条复合语句的组成方式是, 它由一个或者多个子句组合而成。对于其中的每一个子句来说, 它的结构包含了一个句头和一个句体。在这个特定的复合语句里面, 所有子句的头部分, 也就是句头, 它们都在同一个缩进的层级位置上。
针对每一个子句头的起始位置而言, 它会以一个用来做唯一标识的关键字作为开端, 并且在结尾处有一个冒号紧跟着进行结束。至于这个子句体的具体构成, 它是属于一组语句的范围之内, 这一组语句是受到该子句的控制从而形成的。
子句体的形式可以是紧跟着子句头冒号、并且和它在同一行出现的单个简单语句, 也可以是同样在这一行里用分号分开连接的多个简单语句。另外, 子句体还可以是排在子句头下面、且缩进了一层或几行的一个或多个语句。要是想要在这些子句体里面放入嵌套的复合语句, 那就只能用第二种形式, 也就是缩进的那些语句。
第一种形式是不合法的, 原因是系统没有办法搞清楚后面跟着的某个子句到底应该算作前面哪一个子句里面的内容。
if test1: if test2: print(x)
还得注意, 在这种情况下, 分号的绑定关系比冒号的绑定关系要更紧密一些, 所以就出现在下面这个示例里的情况, 所有的执行动作操作要么完全一个都不去进行实施, 要么就是全部都一起去完成实施:
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_listNEWLINE | NEWLINE INDENTstatement+ DEDENT
statement::=stmt_listNEWLINE |compound_stmt
stmt_list::=simple_stmt(";"simple_stmt)* [";"]
请注意, 语句总是以特定的字符结束, 之后可能会跟着另一个特定的字符。还要注意, 可选的后续子句总是以一个不能作为语句开头的关键字作为开头, 因此不会产生歧义。所谓悬空的这种问题, 是通过要求嵌套的语句必须缩进来解决的。
为了保证清晰度, 在下面这几个小节里, 语法规则采用了这样一种方法, 就是把每一个子句都单独放在一个行里面去显示。
8.1. if 语句
把这句话拿来, 用来做那一种需要满足特定条件才能去执行的操作:
if_stmt::= "if"assignment_expression":"suite
("elif"assignment_expression":"suite)*
["else" ":"suite]
它会针对这些表达式一个一个地去算出结果, 一直到碰到一个真值为止, 那个地方请去看一下相关的说明, 那里边有对真值和假值的定义, 就靠着这一步操作, 从很多子句里面挑出一个唯一的匹配项, 接着就开始把这个子句体的内容给执行掉, 与此同时, 这个条件语句里面的其他部分就会被丢掉, 不会被执行, 也不会被算来算去, 要是所有的表达式都算出来是假值的话, 那么只要那个else子句体存在的话, 就会把它给执行掉。
8.2. while 语句
该语句会被用来进行重复性的操作执行动作, 这个操作执行动作的条件背景是表达式的状态需要一直维持在为真的情形之下。
while_stmt::= "while"assignment_expression":"suite
["else" ":"suite]
这是一个将不断重复进行的情况, 也就是要反复地去检验那个表达式, 然后一旦发现这个表达式的值是属于真的那一种状态的, 就去执行第一个部分里面的那些语句代码;但是呢, 如果这个表达式的值是假的, 这种情况甚至有可能在刚刚第一次去检验它的时候就会发生了, 要是这样的存在一个写着else并且带有子句体这样的情况的话, 那就去执行那个else后面的内容。
当在第一个子句体中的 语句被执行的时刻, 它会直接去终止整个循环的动作, 与此同时, 还会明确地不再去执行 else 部分的内容。另外, 在首次提到的 语句发生作用的时候, 它会跳过子句体里面剩下的那些所有的代码片段, 并且会马上返回来重新去校验一下那个表达式的结果。
8.3. for 语句
这个句子的用法是对于序列里的元素进行迭代操作, 这些元素可能存在于像字符串、元组或者列表这样的结构中, 或者是出现在任何其他可以迭代的对象内。
for_stmt::= "for"target_list"in"expression_list":"suite
["else" ":"suite]
表达式列表会被评估, 并且只评估一次。它应该去生成一个可以迭代的东西。系统会基于那个表达式的结果, 去创建一个迭代器。然后, 针对这个迭代器所提供出来的每一项内容, 都会把子句体执行一遍。具体来说, 执行的顺序就跟从迭代器里面获取数据的顺序是一致的。
针对每一项内容, 会根据标准的赋值规则把它依次给到目标列表里面的对应位置去。做完这步之后, 就会去执行相应的子句体。等把所有的那些项目全都耗完之后, 如果出现序列本身就是空着的, 或者是迭代器那边引发出了异常的情况, 这个动作是立刻就会发生的。
要是 else 那一部分的子句体它还存在的, 这时候就会被执行下去, 然后循环就随之终止了。
当第一个子句体中的特定语句被执行的时候, 这个循环就会被终止。同时, else子句体中的内容就不会被执行下去。如果那个语句在第一个子句体中跑动了, 它就会跳过当前子句体里剩下的那些部分。
接着它会把注意力转往下一项, 接着继续进行处理。要是眼下已经没有下一项的话, 它的流程就会转向去执行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
目标列表里面的那些名字, 在循环结束以后并不会被删掉。但是要是序列是空的, 这些名字就压根不会给循环去进行赋值操作。这里有一个小提示, 就是说内置函数会返回一个可以进行迭代的整数序列, 这个功能很适合用来模拟那种for i := a to b do的效果。
打个比方来讲, 比如使用list这种形式加上range(3)这样写, 最后会得到一个包含特定内容的列表< strong>。
注解
当序列在循环中被修改的时候, 会出现一个比较微妙的问题, 这种情况只可能发生在像列表这样可变的序列上面。 会有一个内部的计数器用来跟踪下一个应该被使用的项, 每进行一次迭代, 这个计数器就会产生一次递增操作。 等到计数器的数值达到序列长度的时候, 循环过程就会立即终止。
也就是说, 如果语句体从序列里面删除了当前这一项以及之前已经处理过的那一项的话, 那么下一项就会被直接跳过去, 因为下一项的编号会变成刚才已经处理过的那个当前项的编号。
同样的道理来看, 如果语句体在现在处理的当前项的前面插入一个全新的项的话, 那么当前这项就会在循环进行到下一轮的时候再被重新处理一遍。这种情况下是会产生一些让人头疼的程序错误的, 为了避免发生这些问题, 比较好的解决办法是对整个序列使用切片技术来制作一个临时的副本, 比如你可以通过这样操作。
for x in a[:]:
if x < 0: a.remove(x)
8.4. 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
这个子句指定了一个或者多个异常处理程序。在try子句里面没有发生异常的情况之下, 任何一个异常处理程序都不会被去执行。如果在try子句里面发生了异常, 那么就会开始对异常处理程序进行搜索动作。这一种搜索的方式会把那个子句进行逐一的检查, 直到找到和这个异常正好相匹配的子句为止。
如果存在那种没有表达式的子句的话, 那么这个子句必须得排在最后的位置上, 它会匹配任何的一种异常情况。
针对那个包含了表达式的子句来说, 它会先去把里面的表达式算出来拿到结果对象, 然后会去判断这个结果对象和正在发生的那个异常是不是“兼容”的关系, 如果发现它们确实兼容的话, 那这个子句就会去匹配这个异常。如果要弄清楚到底什么叫做两者之间是“兼容”的, 得看具体的条件。
如果一个对象正好是某个异常对象所属于的那个类, 或者是那个例外情况所属类的基类, 又或者这个对象是一个元组, 而这个元组当中又包含了能够兼容该异常的项, 那么这两者就被认为是兼容的。
如果在没有子句与异常相匹配的情况下, 那么, 相关的代码就会继续进行搜索, 以查找处理异常的处理器, 这个搜索的动作会在周边的代码中出现, 同时也会沿着调用栈向上追溯, 去查看发起调用的那一层是否有能处理该异常的处理器。
假如在处理子句头部里面的表达式的时候, 突然冒出了一个异常, 那么先前对于处理器的这种搜索行为就会被叫停。接下来, 系统会在周围的代码片段以及调用栈上面开展针对这个新异常的搜索工作, 并且把这个新异常当作是由整个语句本身所导致的异常来看待。
在找到一个匹配的子句时, 这个异常就会被赋值给该子句里面, 作为关键词as后面指定的那个目标对象, 前提是如果有的话, 然后接下来执行的, 就是那个匹配上的子句体里面的代码内容。实际上来说的话, 所有的子句, 都必须是带有一个可以运行和处理的子句体才行。
等到最终走到了子句体的末尾那里, 大家通常的做法会是, 直接跳过这一段, 转向整个try语句后面接着去执行后面的其余代码段落。
这指出的实际情况是, 要是存在有针对同样一种异常而设置的嵌套结构的两个处理器, 并且如果异常状况是发生在了内层处理器的 try 语句块部分之中, 那么外层的处理器就不会去对这个异常做出处理的动作。
当人们在使用的过程中把目标用一个异常来进行赋值操作的时候, 这种操作需要让 as 这个关键词来作为媒介进行连接并且发生作用。那么到了该子句结束的那个时刻, 之前的那个赋值操作就会自动失效并且被彻底清理干净, 而这就等于是。
except E as N:
foo
被转写为
except E as N:
try:
foo
finally:
del N
这意味着, 异常对象必须把一个不一样的名字赋值给它, 这样子句结束以后的地方才可以用那个名字把它找到。
之所以要把它们给清理掉, 是因为如果在那些回溯的信息也都带着的情况下去用, 就会让堆栈的框架产生互相引用而形成一种闭环的状态, 这样的话, 所有在那个环境里面的局部变量都会一直活着不消失, 非得等到下一次垃圾回收机制启动的时候才会被处理掉。
当一个子句体执行操作还没完成之前, 涉及到的那些关于错误的具体情况是被存储到了某个特定的程序模块里面的, 而且这些东西是可以被拿出来使用的。
这个地方提供了一个叫做三元组的东西, 它是由错误的类型、错误产生的具体对象, 还有记录错误调用过程的对象这三部分拼在一起的, 具体内容可以参考相关章节的内容, 这么做是为了能够在整个软件运行期间, 清楚地指出是在哪个环节冒出了问题。
如果要从专门负责处理这些错误的函数里面把控制权交出去, 也就是返回的时候, 那个用来表示错误信息的变量的数值, 会重新变回它在调用这个函数之前的样子。
如果控制流在离开那个块的时候, 根本就没有发生错误, 并且在整个过程中也没有执行过那些终止或者转移的流程语句, 那么那个被标记为可选的补充部分就会被启动并运行。与此同时, 当在那个补充部分里面出现问题时, 前面已经说过的模块是不能对这些异常进行处理和解决的。
倘若该结构存在, 那么它就会去设定一个用于清理的处理程序, 于是子句就会得到执行, 其中还包含了任何相关的 和 else 子句, 假如在那些子句里面出现了没有被处理的异常状况, 那么这个异常就会被放置起来暂存一下, 接下来 子句将会被执行, 要是发现确实有被暂时保存起来的异常存在, 它就在 子句的最终阶段被重新激发出来, 但是如果 子句自身又引发了另外一个新的异常情况, 那么之前被保存住的那个异常就会被设定为新出现的那个异常的上下文关系。
如果子句的执行过程里面, 出现了那个被保存下来的异常情况的消失现象, 那么就会是因为在该程序运行的时候, 触发了特定的语句操作, 这种情况下, 原本已经记录在案的异常情况就会被系统直接抛弃掉。
>>> def f():
... try:
... 1/0
... finally:
... return 42
...
>>> f()
42
在子句执行期间, 程序方面是没有能力去获取到那些异常信息的。
当 , 或 语句在一个 try... 语句的 子语句体中被执行时, 子语句也会‘在离开时’被执行。
函数的返回值, 是得由最后被运行那条语句给定出来。因为那个特定的子句老是会被执行, 所以在这个子句里运行语句时, 它就一定是最后一个运行的:
>>> def foo():
... try:
... return 'try'
... finally:
... return 'finally'
...
>>> foo()
'finally'
有关异常方面, 更加具体一些的详细内容, 您是可以去那一节里头进行查找的;再往更进一步的, 关于如何使用相关语句来生成异常的情况和信息, 您同样是可以在另外一节内容里面, 仔细地予以查阅和找到的。
对版本号 3.8, 情况发生了改变。在那个版本之前, 语句是不允许出现在子句里面的。出现这种限制的原因在于, 具体的实现方式存在一个难题, 所以不能那样用。
8.第5点, 就是关于with这个语句。
现在人们所使用的这种写法,主要是针对那些包含有通过上下文管理器进行定义的方法的代码块来执行操作的。具体的情况是可以查阅相关的章节内容。这种做法的好处是, 它能够帮助把一些常见的操作模式给封装起来, 从而实现方便且快捷的重复使用功能。
with_stmt::= "with"with_item(","with_item)* ":"suite
with_item::=expression["as"target]
当一个语句里面带有项目这个部分的时候, 那么关于这句话的具体执行步骤, 接下来会按照以下这种方式来进行呈现:
对那个上下文表达式(也就是由中给出的这个表达式)求值, 然后得到一个上下文管理器。
将内容加载到上下文管理器当中, 这样做的目的是为了能够在后续的时间里方便地进行使用操作。
将内容加载到上下文管理器当中, 这样做的目的是为了能够在后续的时间里方便地进行使用操作。
启动对上下文管理器调用方法的这个工作。
如果这个句子中包含一个目标的话, 那么它就会接收到来自那个返回值的赋值操作。
注解
这个语句会做出这样一种保证, 就是在方法返回的时候, 假如没有发生错误, 那么这个代码块的内容将会一定被调用。所以呢, 假设在对目标列表进行赋值的过程当中产生了错误的现象, 这种错误会被当作是在语句体内部出现的情况来对待。大家可以参照下面提到过的在第 6 步的内容来进行理解。
把执行语句的那个体部分。
触发上下文管理器中那个专门用于发起调用的方法来进行处理, 具体来说就是当整个语句块内部因为出现了异常情况从而被迫终止退出的时候, 那么所对应的异常种类以及它所携带的值还有相关的回溯信息就会被一起打包传送到那里去, 反之如果并没有发生这种意外的情况, 那就默认会直接递送那三个标准的参数过去。
如果语句体的运行过程中的退出, 是因为有异常被触发所导致的, 并且那个来自某个方法内部的返回结果是假的这个状态成立的话, 那么该异常就会被再次地往外抛出。而如果那个返回结果是真的话, 那么这个异常就会被压住、给按住不动了, 紧接着流程会继续走下去, 进入执行紧跟在这后面的一段代码区域。
如果那个语句体是因为异常之外别的什么原因就退出了, 那么来自于某一个地方提供的返回值就会被直接忽略掉, 然后代码会继续在这个类的正常的退出位置接着去执行。
以下代码:
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 A() as a, B() as b:
SUITE
在表达出的意思上面是差不多的。
with A() as a:
with B() as b:
SUITE
在3.1版进行了更改: 允许使用多个上下文表达式。
参见
关于PEP 343中所讲述的“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_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
函数定义, 它其实属于那种可执行的语句, 当你执行这个语句的时候, 在当前这个局部的命名空间里面, 就会把函数的名称绑定到一个叫做函数对象的东西去, 而这个函数对象本质上是对函数可执行代码所做的那个包装器工作, 另外呢, 这个函数对象里面还保留着对当前全局命名空间的引用信息, 这个信息的作用是用来作为该函数在被实际调用的过程中所使用的那个全局命名空间的内容。
函数在定义的过程中, 是不会去执行里面的函数体部分的。真正的执行情况, 是要等到有人把这个函数给调用出来的那一天之后,才会发生的。
可以将一个函数定义包装在由一个或者多个表达式组成的结构里。 当人们去定义这个函数的时刻, 计算机会在那个包含着该函数定义的作用域环境里面, 把对于那些作为装饰器的表达式进行求值操作。
最后算出来的结果必须属于那种可以被呼叫的对象类型, 然后系统会用这个函数对象当作它唯一的参数, 来发起一次呼叫的动作。 这一次呼叫所返回回来的值, 不会被绑在所谓的函数对象上面, 而是会被固定链接到那个函数名称上去。
要是遇到有多个装饰器的情况, 它们应用的方式就像俄罗斯套娃那样, 是以一层嵌套一层的方式去处理的, 比如你看下面这一段代码。
@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
关于函数调用的语义方面, 大家可以在相应的章节里看到这个更加详细的一个描述情况。
在函数调用这个过程当中, 总是会给那个形参列表里面所列出来的这些所有的形式参数进行赋值操作的, 而这种赋值操作, 它要么就是通过位置参数的方式来进行, 要么就是使用关键字参数的这么一个方式来完成, 当然还有可能就直接使用默认的一个数值去进行了。
假如说要是存在那种类似于带有一个星号这样的一个特定形式的, 那这个特定的部分它就会首先被初始化成为一个元组形态的这个东西, 然后让它去接收那些任何多出来的额外位置参数, 并且在没有收到这些额外的参数的情况下, 它的初始值就是一个空的那个元组。
如果存在 这样的形式, 那么它会被初始化为一个新的有序映射来接收任何额外的关键字参数, 而这个新的有序映射默认情况下是一个相同类型的空映射。对于在 * 或者两个星号之后的那些形参来说, 它们都是仅限关键字形参, 所以只能通过使用关键字参数的办法来进行传入。
而在 / 之前的那些形参则是仅限位置形参, 因此就只能通过传递位置参数的方法来进行了。
在 3.8 版更改: 可以使用 / 函数形参语法来标示仅限位置形参。 请参阅 PEP 570 了解详情。
形参可以带有这种内容, 它的具体表现形式就是在形参的名称后面去加上冒号和空格。不管是什么形式的形参都有资格去带有这样的标注这一说, 哪怕是星号或者双星号代表的那种特殊的形参也完全一样。
函数也能够拥有针对返回值的标注, 想要实现这个效果, 就得在形参列表的末尾处去加上减号和大于号组成那个指向右边的箭头符号。这些所使用的标注本身可以是任何被认定为合法的表达式代码, 仅仅只是存在这样的一些标注标记而已, 它绝不会对函数本身的实际运行逻辑或者含义产生任何改变的影响。
标注值可以作为函数对象的属性被访问, 具体的方式是通过一个字典来获取这个值, 而这个字典的特征是以对应的形参名称作为键。如果代码里使用了从模块导入的方式的话, 那么这些标注在程序运行的时候会被保存成字符串的格式, 这样做是为了能够实现延迟求值的这种特性。
假如没有采用这种导入方式的情况存在的话, 它们就必须在执行函数定义的这个动作发生的时候进行求值计算。面对这种特定的情况而言, 对标注本身进行求值的顺序很可能并不一致于它们在源代码文本里所展现出来的先后出现的那些顺序。
人们也能够去创建一个没有绑定到一个具体名字的匿名函数, 然后呢就在表达式里面立刻去使用这种函数。这一做法是得要依赖于 那个表达式的, 关于它的具体说明, 大家可以去看一下相关的章节内容。还需要注意的是, 这里的写法仅仅是一个对简单函数定义的一种简化的描述方式罢了。
同时我们也要明确一点的就是说, 那些在代码语句里面定义的函数, 它们也是可以像那些用相应表达式定义的函数一样, 被传递给其他的地方或者被赋值给其他的名称。
所谓“def”这一种形式, 它的实际能力要比其他的写法更加强大一些, 主要的原因, 就在于它这个方式允许大家在里面执行很多的语句, 同时还可以使用标注功能。
程序人员在写代码的时候需要注意这样一个事项: 函数其实是属于一类特定的对象。如果在一个函数内部去执行那个叫做 “def” 的语句, 那么它就能够定义出一个局部的函数来, 这个定义的局部函数随后可以被返回出去或者是被当作参数传递给别人。
至于那些在嵌套使用的函数里边出现的自由变量, 它们是能够访问到包含了当前这个 def 语句的外层函数的局部变量的。想要知道更多的详细情况的话, 请去查阅相应的那一节内容。
参见
关于PEP 3107, 它是涉及到函数标注的一件事情。
最初的那个函数标注的规范说明, 就是这样的。
这是关于PEP 484的内容, 它讲的是类型提示这一回事。
标注的标准含意定义就是类型提示。
PEP 526 - 变量标注的语法
变量声明里面有一个叫做法的类型提示功能, 这个功能是包括类变量的时候, 也是包括实例变量的时候。
PEP 563 - 延迟的标注求值
我们支持在系统运行的时候, 通过一种特定的方法来实现标注内部的向前引用, 这种方法就是在运行时把标注保存成字符串的形式, 而不是那种即时求值的操作方式, 从而解决了相关问题。
8.第7点, 类所对应的那种定义方式。
类定义指的是对类对象进行的定义, 这一点可以参见相关的这一节内容。
classdef::= [decorators] "class"classname[inheritance] ":"suite
inheritance::= "(" [argument_list] ")"
classname::=identifier
类定义是一个可以执行的句子。在通常情况下, 继承列表这部分内容会给出基类的清单(至于更高级的使用方法, 请参阅相关资料), 这个列表里面的每一项内容都需要被计算出来, 并且结果要是一个允许成为子类的类对象。如果一个类没有写继承列表, 那么这个类就会默认去继承叫做“”的基类;因此:
class Foo:
pass
等价于
class Foo(object):
pass
后来那个类里面的代码, 会被安排在一个新搞出来的执行帧里面去做事情, 这个执行帧大家可以去查看相关章节的内容作为参考, 它会用刚才新弄好的局部名字空间, 还有之前一直在那里的全局名字空间。
一般来说来说, 一个类的内部主要就是放着几个函数的定义内容。当这个类里面的代码全部做完、结束运行的那一刻, 那个用来跑代码的帧就会被丢掉不要了, 但是当时用到的一些局部名字, 也就是局部名字空间, 会被先存下来备着。
紧接着就会去生成一个新类的实体对象, 给它定下父辈名单就用那个继承列表的东西, 给它配上的属性字典呢, 就采用刚才存下来的那个局部名字空间。这个类名称会被放到那个早就存在的全局的命名空间里去, 并且会和那个类的对象形成绑定关系。
在类里面去定义的这些属性的先后次序是被给保存下来了, 这个事是放在那个叫新类的某个什么东西里面的。你要注意的是说这个顺序到底可靠不可靠呢, 它仅仅只是在类刚刚被创建出来的那个时候才靠谱的, 然后它还仅仅只能够对那些用了定义语法所给出的那种类才有效。
在需要对其进行重度定制的时候, 可以选择使用类来进行创建。
类这个东西也可以被进行装饰处理, 它的情况是和装饰函数的状况是一样的。
@f1(arg)
@f2
class Foo: pass
大致等价于
class Foo: pass
Foo = f1(arg)(f2(Foo))
先按照跟函数装饰器是一样的规则, 去对那个装饰器表达式进行求值处理, 然后再把得到的结果, 绑定到类名称上去。
以下是程序员需要注意的事项: 在类定义的地方, 所定义的变量被称为是类属性, 这些东西会被类的各种实例所共享。如果想要设定实例属性的话, 可以在方法内部使用自我名字等于值的这种代码形式来进行操作。
无论是哪一类的属性, 也就是类属性以及实例属性, 都可以借助于自我名字这样的表示法来实现访问功能, 但是有一点值得警惕, 也就是说当采用了此种方式去实现访问的时候, 若存在同名情况的话, 实例属性会把原本对应的类属性给隐藏起来。
类属性可以被当做起实例属性的默认值, 但是, 在这里面, 如果使用一个可变类型的值, 是可能会产生一些预料不到的结果的。可以使用相关的工具或者方式, 来创建出那些实现细节有所不同、彼此独立的实例变量。
参见
PEP 3115 - 3000 中的元类
提出把元类声明的写法改成眼下这一套语法的建议, 同时也给出解释说明, 到底是应该怎么去搭那个带着元类的类的构造过程。
这就是PEP 3129, 也就是类装饰器。
有个提议是要去增加那种类似装饰器的东西, 那个函数和方法的装饰器是早先在 PEP 318 里面被给引入进来的。
8.这就是第八个方面, 叫做协程。
8.关于协程函数的定义, 这是第8.1部分的内容。
async_funcdef::= [decorators] "async" "def"funcname"(" [parameter_list] ")"
["->"expression] ":"suite
协程能够在若干个不同的地点进行挂起操作, 并且可以从这些地点恢复执行, 具体的情况可以参见相关的内容。(参见)。在协程函数的代码体内部, await 关键字和 async 关键字已经变成了保留字。这意味着表达式以及相关的控制流程只能在协程函数的代码体内部来进行使用。
使用 async def 语法定义的函数总是为协程函数,即使它们不包含 await 或 async 关键字。
当你把那个叫做 yield from 的表达式, 放到协程函数内部的代码块中去使用的时候, 系统是会去弄出一个问题的。
协程函数的例子:
async def func(param1, param2):
do_stuff()
await some_coroutine()
8.8.2. async for 语句
async_for_stmt::= "async"for_stmt
因为可以在其迭代过程里面去调那些异步代码, 所以它能够实现遍历, 但是呢, 它的下一个值的获取方式是通过那个 next 方法来进行的, 而且在这个 next 方法里面也是可以调用那些异步操作的。
用户提交的语句, 是可以让我们去对异步迭代器进行一个比较方便的操作的, 这种操作呢, 就是迭代。
以下代码:
async for TARGET in ITER:
SUITE
else:
SUITE2
在表达出的意思上面是差不多的。
iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
try:
TARGET = await type(iter).__anext__(iter)
except StopAsyncIteration:
running = False
else:
SUITE
else:
SUITE2