Bootstrap

【Python学习手册(第四版)】学习笔记11.1-赋值语句(分解赋值、序列赋值、解包等)及变量命名规则详解

个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。

本文主要对赋值语句的各种形式做详解,非常通俗易懂的语言、循序渐进的方式,分别对单个、元组及列表分解、序列赋值、序列解包、多重目标、增强赋值等做详细说明,以及详细的示例。末尾对变量命名规则做了简要说明,更细致需查看PEP 8标准。


目录

赋值语句

赋值语句的形式

单个赋值

元组及列表分解赋值

序列赋值语句

扩展的序列解包

多重目标赋值

增强赋值语句

序列赋值示例

高级序列赋值语句模式

手动分片

扩展序列解包

实际应用

边界情况

解包VS分片

for循环应用

多目标赋值语句示例

多目标赋值以及共享引用

增强赋值语句示例

增强赋值以及共享引用

变量命名规则

命名惯例

变量名没有类型,但对象有


赋值语句

赋值语句是把对象赋给一个名称。基本形式是在等号左边写赋值语句的目标,而要赋值的对象则位于右侧。左侧的目标可以是变量名或对象元素,而右侧的对象可以是任何会计算得到的对象的表达式。

赋值语句有些特性要专门记住的,如下所示。

  • 赋值语句建立对象引用值。Python赋值语句会把对象引用值存储在变量名或数据结构的元素内。赋值语句总是建立对象的引用值,而不是复制对象。
  • 变量名在首次赋值时会被创建。Python会在首次将值(即对象引用值)赋值给变量时创建其变量名。有些(并非全部)数据结构元素也会在赋值时创建(例如,字典中的元素,一些对象属性)。赋值后每当这个变量名出现在表达式时,就会被其所引用的值取代。
  • 变量名在引用前必须先赋值。使用尚未进行赋值的变量名是一种错误。
  • 执行隐式赋值的一些操作。在Python中,赋值语句会在许多情况下使用。例如,模块导入、函数和类的定义、for循环变量以及函数参数全都是隐式赋值运算。赋值语句在任何出现的地方的工作原理都相同,所有这些环境都是在运行时把变量名和对象的引用值绑定起来。

赋值语句的形式

表11-1说明Python中不同的赋值语句的形式。

单个赋值

第一种形式是至今最常见的——把一个变量名(或数据结构元素)绑定到单个对象上。

>>> spam = 'spam'

仅仅使用这些基本的形式就可以搞定所有的工作了。其他的表中的项目代表了程序员通常会觉得很方便的特定的和可选的形式。

元组及列表分解赋值

表中第二和第三种形式是相关的。当在“=”左边编写元组或列表时,Python会按照位置把右边的对象和左边的目标从左至右相配对。

>>> spam,ham = 'yum','YUM'
>>> spam
'yum'
>>> ham
'YUM'
>>> [spam,ham] = ['yum','YUM']
>>> spam
'yum'
>>> ham
'YUM'

例如,表中第二行,字符串'yum'赋值给变量名spam,而变量名ham则绑定至字符串'YUM'。从内部实现上来看,Python会先在右边制作元素的元组,所以这通常被称为元组分解赋值语句。

序列赋值语句

元组和列表赋值语句已统一为现在所谓的序列赋值语句的实例——任何变量名的序列都可赋值给任何值的序列,而Python会按位置一次赋值一个元素。

实际上,我们可以混合和比对涉及的序列类型。例如,表11-1的第四行,把变量名的元组和字符的字符串对应起来:a赋值为's',b赋值为'p'等。

>>> a,b,c,d = 'spam'
>>> a
's'
>>> b
'p'
>>> c
'a'
>>> d
'm'

扩展的序列解包

序列赋值允许我们更灵活地选择要赋值的一个序列的部分。

例如,表11-1中的第五行,用右边的字符串的第一个字母来匹配a,用剩下的部分来匹配b:a赋值为's',b赋值为'pam'。这为手动分片操作的结果的赋值提供了一种简单的替代方法。

>>> a,*b = 'spam'
>>> a
's'
>>> b
['p', 'a', 'm']

多重目标赋值

表11-1的第五行指的是多重目标形式的赋值语句。Python赋值相同对象的引用值(最右边的对象)给左边的所有目标。

>>> spam = ham = 'lunch'
>>> spam
'lunch'
>>> ham
'lunch'
>>> id(spam)
1914446353776
>>> id(ham)
1914446353776

变量名spam和ham两者都赋值成对相同的字符串对象'lunch'的引用。效果就好像我们写成ham='lunch',而后面再写spam=ham,这是因为ham会得到原始的字符串对象(也就是说,它并不是这个对象的独立的拷贝,指向了同一个对象)。

增强赋值语句

表11-1的最后一行是增强赋值语句的例子——一种以简洁的方式结合表达式和赋值语句的简写形式。

>>> spams = 0
>>> spams += 42
>>> spams
42
>>> spams = 0
>>> spams = spams + 42
>>> spams
42

例如,"spam+=42"相当于"spam=spam+42",然而增强形式输入较少,而且通常执行得更快。此外,如果操作主体是可变的并且支持这一操作,增强赋值通过选择原处更新操作而不是对象副本,从而可以更快地运行。在Python中,每个二元表达式运算符都有增强赋值语句。

序列赋值示例

以下是一些序列分解赋值语句的简单例子。

>>> nudge = 1
>>> wnk = 2
>>> a,b = nudge,wnk
>>> a,b
(1, 2)

>>> [c,d] = [nudge,wnk]
>>> c,d
(1, 2)

在这个交互模式下,在第三行写了两个元组,只是省略了它们的括号。

Python把赋值运算符右侧元组内的值和左侧元组内的变量互相匹配,然后每一次赋一个值。

元组赋值语句在Python中有一个常用的编写代码的技巧:

>>> nudge = 1
>>> wnk = 2

>>> nudge,wnk = wnk,nudge
>>> nudge,wnk
(2, 1)

因为语句执行时,Python会建立临时的元组,来存储右侧变量原始的值,分解赋值语句也是一种交换两变量的值,却不需要自行创建临时变量的方式:右侧的元组会自动记住先前的变量的值。

Python中原始的元组和列表赋值语句形式,最后已被通用化,以接受右侧可以是任何类型的序列,只要长度相等即可

可以将含有一些值的元组赋值给含有一些变量的列表,字符串中的字符赋值给含有一些变量的元组。在通常情况下,Python会按位置,由左至右,把右侧序列中的元素赋值给左侧序列中的变量。

>>> [a,b,c] = [1,2,3]
>>> a,c
(1, 3)
>>> a
1
>>> a,b,c = 'ABC'
>>> a,c
('A', 'C')
>>> a
'A'
>>> (a,b,c) = 'ABC'
>>> a,c
('A', 'C')
>>> a
'A'

序列赋值语句实际上支持右侧任何可迭代的对象,而不仅局限于任何序列。这是更为通用的概念。

高级序列赋值语句模式

虽然可以在“=”符号两侧混合相匹配的序列类型,右边元素的数目还是要跟左边的变量的数目相同,不然会产生错误。

>>> string = 'life'
>>> a,b,c,d = string
>>> a,c
('l', 'f')
>>> a,b,c = string
Traceback (most recent call last):
  File "<pyshell#50>", line 1, in <module>
    a,b,c = string
ValueError: too many values to unpack (expected 3)

Python 3.0允许我们使用更为通用的扩展解包语法。

手动分片

想要更通用的话,就需要使用分片了。这里有几种方式使用分片运算。

>>> a,b,c = string[0], string[1], string[2:]
>>> a,b,c
('l', 'i', 'fe')

>>> a,b,c = list(string[:2]) + [string[2:]]
>>> a,b,c
('l', 'i', 'fe')

>>> a,b = string[:2]
>>> c = string[2:]
>>> a,b,c
('l', 'i', 'fe')

>>> (a,b),c = string[:2], string[2:]
>>> a,b,c
('l', 'i', 'fe')

最后的例子有点难理解,因为在这里甚至可以赋值嵌套序列,而Python会根据其情况分解其组成部分,就像预期的一样。

达成的效果就是:赋值元组中的两个项目,而第一个项目是嵌套的序列(字符串),如下例:

>>> ((a,b),c) = ('li', 'fe')
>>> a,b,c
('l', 'i', 'fe')

Python先把右边的第一个字符串('li')和左边的第一个元组((a,b))配对,然后一次赋值一个字符,接着再把第二个字符串('fe')一次赋值给变量c。

在这个过程中,左边对象的序列嵌套的形状必须符合右边对象的形状。像这种嵌套序列赋值运算是比较高级,也很少见到的,但是利用已知形状取出数据结构的组成成分,也是很方便的。

这一技术在for循环中也有效,因为循环项赋值给了在循环头部给定的目标:

>>> for (a,b,c) in [(1,2,3),(2,3,4)]: ...


>>> for ((a,b),c) in [((1,2),3),((2,3),4)]: ...

序列解包赋值语句也会产生另一种Python常见的用法,也就是赋值一系列整数给一组变量

>>> red,green,blue = range(3)
>>> red,green
(0, 1)

把三个变量名的初始值设为整数0、1以及2。这里的range内置函数会产生连续整数,range一般用于循环中。

另一个元组赋值语句的地方就是,在循环中把序列分割为开头和剩余两部分,如下所示。

>>> l = [1,2,3,4]
>>> while l:
	front,l = l[0], l[1:]
	print(front,l)

	
1 [2, 3, 4]
2 [3, 4]
3 [4]
4 []

循环的元组的赋值语句也能编写成下列两行,但放在一起使用,往往要更方便一些。

>>> while l:
    front = l[0]
    l = l[1:]
	print(front,l)

这个程序使用列表作为一种堆栈的数据结构。这种事我们通常也能用列表对象的append和pop方法来实现。在这里,front=L.pop(0)和元组赋值语句有相当的效果,但这是在原处进行的修改。

扩展序列解包

序列赋值是更为通用的。一个带有单个星号的名称,可以在赋值目标中使用,以指定对于序列的一个更为通用的匹配——一个列表赋给了带星号的名称,该列表收集了序列中没有赋值给其他名称的所有项。

那种把一个序列划分为其“前面”和“剩余”部分的常用编码模式,这种方法特别方便。

实际应用

来看一个示例。序列赋值如果长度不同的话,将会得到一个错误(除非像前面所介绍的那样手动地在右边分片):

>>> seq = [1,2,3]
>>> a,b = seq
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
    a,b = seq
ValueError: too many values to unpack (expected 2)

可以在目标中使用带单个星号的名称来更通用地匹配。在如下的后续交互会话中,a匹配序列中的第一项,b匹配剩下的内容:

>>> a,*b = seq

>>> a
1
>>> b
[2, 3]

当使用一个带星号的名称的时候,左边的目标中的项数不需要与主体序列的长度匹配。实际上,带星号的名称可以出现在目标中的任何地方。例如,在下面的交互中,b匹配序列中的最后一项,a匹配最后一项之前的所有内容:

>>> *a,b = seq
>>> a
[1, 2]
>>> b
3

当带星号的名称出现在中间,它收集其他列出的名称之间的所有内容。在下面的交互中,第一项和最后一项分别赋给了a和c,而b获取了二者之间的所有内容:

>>> a,*b,c = seq
>>> a
1
>>> b
[2]
>>> c
3

>>> seq1 = [1,2,3,4,5,6]
>>> a,*b,c = seq1
>>> a
1
>>> b
[2, 3, 4, 5]
>>> c
6

不管带星号的名称出现在哪里,包含该位置的每个未赋值名称的一个列表都将赋给它:

>>> a,b,*c = seq1
>>> a
1
>>> b
2
>>> c
[3, 4, 5, 6]

和常规的序列赋值一样,扩展的序列解包语法对于任何序列类型都有效,而不只是对列表有效。下面,它解包一个字符串中的字符:

>>> a,*b = 'life'
>>> a,b
('l', ['i', 'f', 'e'])

>>> a,*b,c = 'life'
>>> a,b,c
('l', ['i', 'f'], 'e')

>>> a,b,*c = 'life'
>>> a,b,c
('l', 'i', ['f', 'e'])

这和分片内在的相似,但是不完全相同——一个序列解包赋值总是返回多个匹配项的一个列表,而分片把相同类型的一个序列作为分片的对象返回

>>> s = 'life'
>>> s[0],s[1:]
('l', 'ife')

>>> s[0],s[1:3],s[3]
('l', 'if', 'e')

使用这一解包方法,再处理前面小节的最后一个例子的列表,不必手动地分片来获取第一项和剩下的项:

>>> l = [1,2,3,4]
>>> while l:
	front,*l = l
	print(front,l)

	
1 [2, 3, 4]
2 [3, 4]
3 [4]
4 []

边界情况

扩展的序列解包很灵活,一些边界情况还是值得注意。

1、带星号的名称可能只匹配单个的项,但是总是会向其赋值一个列表:

>>> seq
[1, 2, 3]
>>> a,b,*c = seq
>>> print(a,b,c)
1 2 [3]

2、如果没有剩下的内容可以匹配带星号的名称,它会赋值一个空的列表,不管该名称出现在哪里。

如下所示,a、b、c已经匹配了列表中的每一项,但Python会给d赋值一个空的列表,而不是将其作为错误情况对待:

>>> a,b,c,*d = seq
>>> print(a,b,c,d)
1 2 3 []

>>> a,*d,b,c = seq
>>> print(a,b,c,d)
1 2 3 []

3、如果有多个带星号的名称,或者如果值少了而没有带星号的名称(像前面一样),以及如果带星号的名称自身没有编写到一个列表中,都将会引发错误:

>>> a,*b,c,*d = seq
SyntaxError: multiple starred expressions in assignment

>>> a,b = seq
Traceback (most recent call last):
  File "<pyshell#123>", line 1, in <module>
    a,b = seq
ValueError: too many values to unpack (expected 2)

>>> *a = seq
SyntaxError: starred assignment target must be in a list or tuple

>>> *a, = seq
>>> a
[1, 2, 3]

解包VS分片

扩展的序列解包赋值只是一种便利形式。我们通常可以用显式的索引和分片实现同样的效果。但是,扩展的解包更容易编写。

例如,常用的“第一个,其余的”分片编码模式可以用任何一种方式来编写,但是,分片还包括其他的工作:

>>> a,*b = seq
>>> a,b
(1, [2, 3])

>>> a,b = seq[0],seq[1:]
>>> a,b
(1, [2, 3])

常见的“剩下的,最后一项”分隔模式类似地也可以用任何一种方式来编写,但是扩展解包语法显然要减少很多录入:

>>> *a,b = seq
>>> a,b
([1, 2], 3)

>>> a,b = seq[:-1],seq[-1]
>>> a,b
([1, 2], 3)

扩展的序列解包语法不仅更简单,而且更自然,使用更加广泛。

for循环应用

for循环语句中的循环变量可能是任何赋值目标,扩展的序列赋值在这里也有效。

扩展赋值可能出现在单词for之后,而更常见的用法是一个简单的变量名称:

当在这种环境中使用的时候,在每次迭代中,Python直接把下一个值的元组分配给名称的元组。

例如,在第一次循环中,就好像运行如下的赋值语句:

名称a、b和c可以在循环的代码中用来引用提取的部分。实际上,这不是特殊情况,只是通用的赋值用法的一种情况。

多目标赋值语句示例

多目标赋值语句就是直接把所有提供的变量名都赋值给右侧的对象。例如,下面把三个变量a、b和c赋值给字符串'spam':

>>> a = b = c = 'spam'
>>> a,b,c
('spam', 'spam', 'spam')

这种形式相当于这三个赋值语句,但更为简单:

>>> c = 'spam'
>>> b = c
>>> a = b

多目标赋值以及共享引用

在这里只有一个对象,由三个变量共享(全都指向内存内同一对象)。这种行为对于不可变类型而言并没问题。

例如,把一组计数器初始值设为零:

>>> a = b = 0
>>> b = b + 1
>>> a,b
(0, 1)

在这里,修改b只会对b发生修改,因为数字不支持在原处的修改。只要赋值的对象是不可变的,即使有一个以上的变量名使用该对象也无所谓

不过,就像往常一样,把变量初始值设为空的可变对象时(诸如列表或字典),就要注意:

>>> a = b = []
>>> b.append(44)
>>> a,b
([44], [44])

因为a和b引用相同的对象,通过b在原处附加值上去,而通过a也会看见所有的效果。

为避免这个问题,要在单独的语句中初始化可变对象,以便分别执行独立的常量表达式来创建独立的空对象:

>>> a = []
>>> b = []
>>> b.append(44)
>>> a,b
([], [44])

增强赋值语句示例

增强赋值语句,是从C语言借鉴而来,而这些格式大多数只是二元表达式和赋值语句的组合简写

例如,下面的两种格式现在大致相等:

增强赋值语句适用于任何支持隐式二元表达式的类型。

例如,下面是两种让变量名增加1的方式。

>>> x = 1
>>> x = x+1
>>> x
2
>>> x += 1
>>> x
3

用于字符串时,增强形式会改为执行合并运算。在这里第二行就相当于输入较长的S=S+"SPAM":

>>> s = 'spam'
>>> s += 'SPAM'
>>> s
'spamSPAM'

如表11-2所示,每个Python二元表达式的运算符(每个运算符在左右两侧都有值),都有对应的增强赋值形式。例如,X*=Y执行乘法并赋值,X>>=Y执行向右位移并赋值。

增强赋值语句有三个优点:

  1. 程序员输入减少
  2. 左侧只需计算一次。在X+=Y中,X可以是复杂的对象表达式。在增强形式中,则只需计算一次。在完整形式X=X+Y中,X出现两次,必须执行两次。因此,增强赋值语句通常执行得更快。
  3. 优化技术会自动选择。对于支持原处修改的对象而言,增强形式会自动执行原处的修改运算,而不是相比来说速度更慢的复制。

最后一点需要多一点的说明:就增强赋值语句而言,在原处的运算可作为一种优化而应用在可变对象上。

比如列表可以用各种方式扩展。要增加单个的元素到列表末尾时,可以合并或调用append:

>>> l = [1,2]
>>> l = l +[3]
>>> l
[1, 2, 3]

>>> l.append(4)
>>> l
[1, 2, 3, 4]

要把一组元素增加到末尾,可以再次使用合并,也可以使用分片赋值语句(例如,L[len(L):]=[11,12,13]),或者调用列表的extend方法:

>>> l = l +[5,6]
>>> l
[1, 2, 3, 4, 5, 6]

>>> l.extend([7,8])
>>> l
[1, 2, 3, 4, 5, 6, 7, 8]

在两种情况下,合并对共享对象引用产生的副作用可能会更小,但是,通常会比对等的原处形式运行得更慢。合并操作必须创建一个新的对象,把左侧的复制到列表中,然后再把右侧的复制到列表中。相比而言,原处方法调用直接在一个内存块末尾添加项。

使用增强赋值语句来扩展列表时,可以忘记这些细节。

例如,Python会自动调用较快的extend方法,而不是使用较慢的“+”合并运算。

>>> l += [9,10]
>>> l
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

增强赋值以及共享引用

这隐含了“+=”对列表是做原处修改的意思。完全不像“+”合并,总是生成新对象。

就所有的共享引用情况而言,只有其他变量名引用的对象被修改,其差别才可能体现出来:

>>> l = [1,2]
>>> m = l     #m新引用该列表
>>> l = l +[3,4]    #+ 连接产生了一个新的对象
>>> l,m    #改变了l,m不变
([1, 2, 3, 4], [1, 2])

>>> l = [1,2]
>>> m = l
>>> l += [3,4]    #+= 等同于extend
>>> l,m    #m引用的原对象也发生了变化
([1, 2, 3, 4], [1, 2, 3, 4])

这只对于列表和字典这类可变对象才重要。如果你需要打破共享引用值的结构,就要对可变对象进行拷贝。

变量命名规则

介绍了赋值语句后,可以对变量名的使用做更正式的介绍。在Python中,当为变量名赋值时,变量名就会存在。但是,为程序中的事物选择变量名时,要遵循如下规则。

语法:(下划线或字母)+(任意数目的字母、数字或下划线)

变量名必须以下划线或字母开头,而后面接任意数目的字母、数字或下划线。_spam、spam以及Spam_1都是合法的变量名,但1_Spam、spam$以及@#!则不是。

区分大小写:SPAM和spam并不同

Python程序中区分大小写,包括创建的变量名以及保留字。就可移植性而言,大小写在导入的模块文件名中也很重要,即使是在文件系统不分大小写的平台上也是如此。

禁止使用保留字

定义的变量名不能和Python语言中有特殊意义的名称相同。例如,如果使用像class这样的变量名,Python会引发语法错误,但允许使用kclass和Class作为变量名。

表11-3列出当前Python中的保留字。

除了大小写是混合的,表11-3中的前三项True、False和None的含义多少有些特殊,出现在Python的内置函数作用域中,并且它们也是可以分配给对象的技术名称。

在所有其他的含义下,它们确实都是保留字,并且,在脚本中,除了它们所表示的对象之外,不可用于任何其他的用途。所有其他的保留字在Python的语法中都是固定的,只能在其本意的特定环境中使用。

此外,因为import语句中的模块变量名会变成脚本中的变量,这种限制也会扩展到模块的文件名:你可以写and.py和my-code.py这类文件。但是你无法将其导入,因为其变量名没有".py"扩展名时,就会变成代码中的变量。

必须遵循刚才所提到的所有变量规则。保留字是禁区,破折号不行,不过下划线可以。

命名惯例

除了这些规则外,还有一组命名惯例——这些并非是必要的规则,但一般在实际中都会遵守。

例如,因为变量名前后有下划线时(例如,__name__),通常对Python解释器都有特殊意义,应该避免让变量名使用这种样式。

以下是Python遵循的一些惯例。

  • ·以单一下划线开头的变量名(_X)不会被from module import*语句导入。
  • ·前后有下划线的变量名(__X__)是系统定义的变量名,对解释器有特殊意义。
  • ·以两下划线开头、但结尾没有两个下划线的变量名(__X)是类的本地(“压缩”)变量。
  • ·通过交互模式运行时,只有单个下划线的变量名(_)会保存最后表达式的结果。

除了这些Python解释器的惯例外,还有Python程序员通常会遵循的各种其他惯例。

例如,类变量名通常以一个大写字母开头,而模块变量名以小写字母开头。此外,变量名self虽然并非保留字,但在类中一般都有特殊的角色。

后面文章会研究另一种更大类型的变量名,称为内置变量名,这些是预先定义的变量名,但并非保留字(所以,可以重新赋值:open=42行得通,不过有时你可能会希望不能这样做)。

变量名没有类型,但对象有

这是让Python的变量名和对象保持鲜明差异的重点所在。

对象有类型(例如,整数和列表),并且可能是可变的或不可变的。另一方面,变量名(变量)只是对象的引用值。没有不可变的观念,也没有相关联的类型信息,除了它们在特定时刻碰巧所引用的对象的类型。

在不同时刻把相同的变量名赋值给不同类型的对象,程序允许这样做:

变量名的这种通用化的本质,是Python程序设计具有的决定性的优点。

变量名也会存在于所谓的作用域内,作用域定义了变量名在哪里可以使用;对一个名字赋值的地点,决定了它在哪里可见。这个后面文章详解,这里不深入。

注意:要了解其他的命名建议,参见Python的半官方风格指南PEP 8中的"Naming conventions"。地址:http://www.python.org/dev/peps/pep-0008

或者搜索"Python PEP 8"。这个文档把Python库代码的编码标准形式化了。

PEP 8所附带的细节比本文目前为止所介绍的要更详细。并且,它变得比需要的更为复杂、严格和主观——其一些建议根本没有被实际工作的Python程序员普遍接受和遵守。

此外,使用Python的一些公司,它们有自己的不同的编码标准

PEP 8确实包含了Python知识中的一些有用的规则,并且,对于Python使用来说,它是很好的读物,只要你把它的推荐当做指南,而不是真理

;