7.2. 赋值语句

赋值语句用于将名称(重)绑定到特定值,以及修改属性或可变对象的成员项:

  1. assignment_stmt ::= (target_list "=")+ (starred_expression | yield_expression)
  2. target_list ::= target ("," target)* [","]
  3. target ::= identifier
  4. | "(" [target_list] ")"
  5. | "[" [target_list] "]"
  6. | attributeref
  7. | subscription
  8. | slicing
  9. | "*" target

(请参阅 原型 一节了解 属性引用, 抽取切片 的句法定义。)

赋值语句会对指定的表达式列表进行求值(注意这可能为单一表达式或是由逗号分隔的列表,后者将产生一个元组)并将单一结果对象从左至右逐个赋值给目标列表。

赋值是根据目标(列表)的格式递归地定义的。 当目标为一个可变对象(属性引用、抽取或切片)的组成部分时,该可变对象必须最终执行赋值并决定其有效性,如果赋值操作不可接受也可能引发异常。 各种类型可用的规则和引发的异常通过对象类型的定义给出(参见 标准类型层级结构 一节)。

对象赋值的目标对象可以包含于圆括号或方括号内,具体操作按以下方式递归地定义。

  • 如果目标列表为后面不带逗号、可以包含于圆括号内的单一目标,则将对象赋值给该目标。

  • 否则:该对象必须为具有与目标列表相同项数的可迭代对象,这些项将按从左至右的顺序被赋值给对应的目标。

    • 如果目标列表包含一个带有星号前缀的目标,这称为“加星”目标:则该对象至少必须为与目标列表项数减一相同项数的可迭代对象。 该可迭代对象前面的项将按从左至右的顺序被赋值给加星目标之前的目标。 该可迭代对象末尾的项将被赋值给加星目标之后的目标。 然后该可迭代对象中剩余项的列表将被赋值给加星目标(该列表可以为空)。

    • 否则:该对象必须为具有与目标列表相同项数的可迭代对象,这些项将按从左至右的顺序被赋值给对应的目标。

对象赋值给单个目标的操作按以下方式递归地定义。

  • 如果目标为标识符(名称):

    • 如果该名称未出现于当前代码块的 globalnonlocal 语句中:该名称将被绑定到当前局部命名空间的对象。

    • 否则:该名称将被分别绑定到全局命名空间或由 nonlocal 所确定的外层命名空间的对象。

如果该名称已经被绑定则将被重新绑定。 这可能导致之前被绑定到该名称的对象的引用计数变为零,造成该对象进入释放过程并调用其析构器(如果存在)。

  • 如果该对象为属性引用:引用中的原型表达式会被求值。 它应该产生一个具有可赋值属性的对象;否则将引发 TypeError。 该对象会被要求将可赋值对象赋值给指定的属性;如果它无法执行赋值,则会引发异常 (通常应为 AttributeError 但并不强制要求)。

注意:如果该对象为类实例并且属性引用在赋值运算符的两侧都出现,则右侧表达式 a.x 可以访问实例属性或(如果实例属性不存在)类属性。 左侧目标 a.x 将总是设定为实例属性,并在必要时创建该实例属性。 因此,a.x 的两次出现不一定指向相同的属性:如果右侧表达式指向一个类属性,则左侧表达式会创建一个新的实例属性作为赋值的目标:

  1. class Cls:
  2. x = 3 # class variable
  3. inst = Cls()
  4. inst.x = inst.x + 1 # writes inst.x as 4 leaving Cls.x as 3

此描述不一定作用于描述器属性,例如通过 property() 创建的特征属性。

  • 如果目标为一个抽取项:引用中的原型表达式会被求值。 它应当产生一个可变序列对象(例如列表)或一个映射对象(例如字典)。 接下来,该抽取表达式会被求值。

如果原型为一个可变序列对象(例如列表),抽取应产生一个整数。 如其为负值,则再加上序列长度。 结果值必须为一个小于序列长度的非负整数,序列将把被赋值对象赋值给该整数指定索引号的项。 如果索引超出范围,将会引发 IndexError (给被抽取序列赋值不能向列表添加新项)。

如果原型为一个映射对象(例如字典),抽取必须具有与该映射的键类型相兼容的类型,然后映射中会创建一个将抽取映射到被赋值对象的键/值对。 这可以是替换一个现有键/值对并保持相同键值,也可以是插入一个新键/值对(如果具有相同值的键不存在)。

对于用户定义对象,会调用 setitem() 方法并附带适当的参数。

  • 如果目标为一个切片:引用中的原型表达式会被求值。 它应当产生一个可变序列对象(例如列表)。 被赋值对象应当是一个相同类型的序列对象。 接下来,下界与上界表达式如果存在的话将被求值;默认值分别为零和序列长度。 上下边界值应当为整数。 如果某一边界为负值,则会加上序列长度。 求出的边界会被裁剪至介于零和序列长度的开区间中。 最后,将要求序列对象以被赋值序列的项替换该切片。 切片的长度可能与被赋值序列的长度不同,这会在目标序列允许的情况下改变目标序列的长度。

CPython implementation detail: 在当前实现中,目标的句法被当作与表达式的句法相同,无效的句法会在代码生成阶段被拒绝,导致不太详细的错误信息。

虽然赋值的定义意味着左手边与右手边的重叠是“同时”进行的(例如 a, b = b, a 会交换两个变量的值),但在赋值给变量的多项集 之内 的重叠是从左至右进行的,这有时会令人混淆。 例如,以下程序将会打印出 [0, 2]:

  1. x = [0, 1]
  2. i = 0
  3. i, x[i] = 1, 2 # i is updated, then x[i] is updated
  4. print(x)

参见

  • PEP 3132 - 扩展的可迭代对象拆包
  • *target 特性的规范说明。

7.2.1. 增强赋值语句

增强赋值语句就是在单个语句中将二元运算和赋值语句合为一体:

  1. augmented_assignment_stmt ::= augtarget augop (expression_list | yield_expression)
  2. augtarget ::= identifier | attributeref | subscription | slicing
  3. augop ::= "+=" | "-=" | "*=" | "@=" | "/=" | "//=" | "%=" | "**="
  4. | ">>=" | "<<=" | "&=" | "^=" | "|="

(请参阅 原型 一节了解最后三种符号的句法定义。)

增强赋值语句将对目标和表达式列表求值(与普通赋值语句不同的是,前者不能为可迭代对象拆包),对两个操作数相应类型的赋值执行指定的二元运算,并将结果赋值给原始目标。 目标仅会被求值一次。

增强赋值语句例如 x += 1 可以改写为 x = x + 1 获得类似但并非完全等价的效果。 在增强赋值的版本中,x 仅会被求值一次。 而且,在可能的情况下,实际的运算是 原地 执行的,也就是说并不是创建一个新对象并将其赋值给目标,而是直接修改原对象。

不同于普通赋值,增强赋值会在对右手边求值 之前 对左手边求值。 例如,a[i] += f(x) 首先查找 a[i],然后对 f(x) 求值并执行加法操作,最后将结果写回到 a[i]

除了在单个语句中赋值给元组和多个目标的例外情况,增强赋值语句的赋值操作处理方式与普通赋值相同。 类似地,除了可能存在 原地 操作行为的例外情况,增强赋值语句执行的二元运算也与普通二元运算相同。

对于属性引用类目标,针对常规赋值的 关于类和实例属性的警告 也同样适用。

7.2.2. 带标注的赋值语句

标注 赋值就是在单个语句中将变量或属性标注和可选的赋值语句合为一体:

  1. annotated_assignment_stmt ::= augtarget ":" expression ["=" expression]

与普通 赋值语句 的差别在于仅有单个目标且仅有单个右手边的值才被允许。

对于将简单名称作为赋值目标的情况,如果是在类或模块作用域中,标注会被求值并存入一个特殊的类或模块属性 annotations 中,这是一个将变量名称(如为私有会被移除)映射到被求值标注的字典。 此属性为可写并且在类或模块体开始执行时如果静态地发现标注就会自动创建。

对于将表达式作为赋值目标的情况,如果是在类或模块作用域中,标注会被求值,但不会保存。

如果一个名称在函数作用域内被标注,则该名称为该作用域的局部变量。 标注绝不会在函数作用域内被求值和保存。

如果存在右手边,带标注的赋值会在对标注求值之前(如果适用)执行实际的赋值。 如果用作表达式目标的右手边不存在,则解释器会对目标求值,但最后的 setitem()setattr() 调用除外。

参见

  • PEP 526 - 变量标注的语法
  • 该提议增加了标注变量(也包括类变量和实例变量)类型的语法,而不再是通过注释来进行表达。

  • PEP 484 - 类型提示

  • 该提议增加了 typing 模块以便为类型标注提供标准句法,可被静态分析工具和 IDE 所使用。