最近在对python的zip()函数的深入分析中,了解到*zip()操作,*a、zip(*a)等操作,在进行一番研究后,得出一些心得体会,通过本文分享如下。
一、首先建立一些概念
python中,我们用各种表达式编辑出的变量,有的变量能够显示具体内容,有的变量只能显示成一个内存地址的形式,有的表达出来直接就报错,但可以print,为方便下一步具体分析,我自定义几个概念:
1、直接输出状态
【直接输出状态】:指该变量/表达式在控制台输入后,可以返回的形式,具体可以有【可显示】、【可表达】、【不可表达】三种。
【可显示】:指该变量/表达式在控制台输入后,返回的是一个具体的内容。
如:
或
【可表达】:指该变量/表达式在控制台输入后,返回显示成一个内存地址的形式
如:
或
【不可表达】:指该变量/表达式在控制台输入后,会报错
如:
或
2、打印输出状态
【打印输出状态】:指在控制台将变量/表达式放进print()函数中,可以输出的形式。
【打印可显示】:指在控制台将变量/表达式放进print()函数中,输出的是一个具体的内容。
如:
或
【打印可表达】:指在控制台将变量/表达式放进print()函数中,输出成一个内存地址的形式。
如
或
【打印不可表达】:指在控制台将变量/表达式放进print()函数中,会报错。这种就说明表达式本身有问题,基本不需要讨论了。
二、*操作符:将最外面的一层结构分离
1、分离效果
设
a = [1,2,3,4]
a是基本的数据结构(列表)
因为*操作符会起到将最外面的一层结构分离的效果
所以*a就会将a最外面一层结构(列表)分离,从一个列表[1,2,3,4],变成四个数字1,2,3,4
由于是四个数字,而非一个统一的结构,所以*a的直接输出状态为【不可表达】
但是由于print()函数是可以接受多个参数的,所以print(*a)可以将四个数字打印出来。*a的打印输出状态为【打印可显示】
注意:
大家可能会有一个疑问
若a=1,2,3,4
会发现a的直接输出状态为【可显示】,
这是因为python的语法特点:当把逗号分隔的表达式赋值给变量的时候,python自动默认添加一对括号,把它变成元组再赋值
即python运行
a=1,2,3,4
的时候,自动“脑补”成
a=(1,2,3,4)
2、*的后接操作
因为*操作符会起到将最外面的一层结构分离的效果,所以*之后的结果默认是多个独立的个体,所以*操作之后的结果可以接下一步需要多个参数的操作(比如需要多个参数的函数)
如
或
3、特殊情况
那么若*a将a最外面一层结构分离之后,里面恰好只有一个数字/数据结构,*a是否【可显示】呢?
如
a=[1]
或
a=[(1,2,3,4)]
经过实际验证发现也是不可以,会报错的。
我的理解是:
python要求表达式最后的结果或者赋值的结果必须是一个统一的结构(逗号分隔的表达式看起来是多个结果,实际是被python“脑补”成了元组)。由于做*操作的时候,不能保证得到的是一个统一的结构,python在编译的时候如果检查到*是最后一步操作,就直接报错。
还有一种特殊情况,在创建元组的时候,(*a)也是会报错的,为什么呢?
我的理解是:
因为元组的创建操作(x1,x2,……)虽然可以接受多个参数,但是必须要显式的分隔符形式才能触发python的编译器将()理解为元组的创建操作,(*a)的(),python会编译为调整计算优先级的普通括号,普通括号里面又只能接一个结构/参数。
4、总结
1、*可将被操作变量/表达式的最外面的一层结构分离,
2、*不能作为最后一步操作(或者赋值前最后一步操作)
3、*操作之后的结果是【不可表达】但【打印可显示】的
4、*操作之后的结果可以接下一步需要多个参数的操作(比如需要多个参数的函数)
5、*之后接的操作如果编译有两种解释的可能(一种多参数,一种单参数),且必须要显式逗号分隔符形式的多参数才能触发编译器将这个操作解释为多参数操作的情况下,*之后接的操作将会因编译器解释为单参数操作而报错。
三、zip():先分离再组装再组装
1、本质效果
第一步:将函数输入的一个或多个参数都分离掉最外面一层结构,某个参数没有外层数据结构就会报错。
第二步:将每个参数分离之后的个体一一对应组装成多个元组,以个体最少的参数为限制,其他参数中无法对应的个体将全部被舍弃。
第三步:将多个元组组装成对象的形式,该形式【可表达】、【打印可表达】
示例
a = [1,2,3,4]
c = [5,6,3,2,1,7,8]
zip(a,c)
print(zip(a,c))
参数没有外层数据结构就会报错:
2、zip()后接操作
zip()的返回值最外层结构是一个对象,原则上很多后接操作都是可以的,而有意义的后接操作有如下几种
变更最外层结构的操作如list()、tuple()
如
或
分离最外层结构的操作,如*、zip()
如
或
3、zip()对象的不稳定性
如果用一个变量来存zip()的返回值,d=zip(a,c)
我们会发现一个比较奇怪的现象
d=zip(a,c)
print(d)
list(d)
print(d)
list(d)
我们发现两次list()操作,d的地址没有变,但是内容第二次没有了,
我的理解是:
任何针对zip()的返回对象的读取内容操作,都需要变更或者分离对象的最外层数据结构,这种读取操作是破坏性的,一旦对象的最外层数据结构被变更或者分离,其内容将不复存在。
从程序的健壮性考虑,尽量不要把ZIP()的返回对象用变量来存储。
4、总结
1、zip()操作会分离掉所有参数的最外层数据结构,若某个参数没有最外层数据结构就会报错。
2、zip()将每个参数分离之后的个体一一对应组装成多个元组,以个体最少的参数为限制,其他参数中无法对应的个体将全部被舍弃。
3、zip()将多个元组组装成对象的形式,该形式【可表达】、【打印可表达】
4、zip()返回对象的内部元素必然是元组
5、zip()的后接操作一般都是会变更或者分离其最外层数据结构的,如list()、tuple()、*。
6、若将zip()返回对象保存为变量,则对该变量内容的读取只能是一次性的,从程序的健壮性考虑,尽量不要把zip()的返回对象用变量来存储。
四、*是不是zip()的逆操作
1、说法不严谨
根据上文可知,
*操作:是一个单纯的分离最外层结构的操作
zip():一次分离,两次组装
1、先将参数最外层结构分离
2、将各参数个体一一对应组装成元组
3、将各元组组装成对象
所以我认为说*是zip()的逆操作是不严谨的。
2、*准确的逆操作
*的效果是单纯的分离最外层结构,他的逆操作就是添加一层外层结构,由于*操作之后被分离的外层结构信息已经丢失,故必须假定知道原最外层结构才能逆回去。
假设x是list,*的逆操作是[]
x=[*x]
假设x是tuple,由于上文讲到直接(*x)会报错,*的逆操作可以是tuple([]),
x= tuple([*x])
x是其他情况的以此类推。
3、zip()准确的逆操作
根据上文描述zip()实际效果的三个步骤,逆操作需要将三个步骤反过来
1、将对象分离成多个元组
2、将每个元组分离成单个元素
3、循环操作:将每个元组第i个元素组合成某种数据结构(因为zip()时候最外层结构被分离已经丢失,这里必须假定知道原最外层结构)
4、返回这多个数据结构(因为zip()会舍弃超长数据结构后面的元素,这里还必须假定原来每一个数据结构长度一致,没有因舍弃造成丢失)。
由于比较复杂,适合写成一个自定义函数,这里假设原最外层结构是list
a = [1,2,3,4]
c = [5,6,3,2]
x=zip(a,c)
def unzip(x):
l1=[*x] #因为zip()返回对象的不稳定性,第二次运行函数会报错
m=len(l1) #原每一个结构有多少个元素
n=len(l1[0]) #原来有多少个数据结构
l2=[]#若原结构不是列表,这里就要改
for i in range(n):
l2.append([])
for j in range(m):
l2[i].append(l1[j][i])
return(l2)
因为多个个体不能作为函数的返回值,为方便操作,将多个个体合成列表再返回,所以最后还要一个*操作符,才算是精确逆操作。
由于*操作符之后的结果是【不可表达】,【打印可显示】,为了方便验证,需要通过print()
由结果可知:
在原参数是列表,原参数长度一致的条件下,*unzip()是zip()的精确逆操作