一、元组(tuple)数据类型讲解
1.元组是什么
python的元组是有序且不可被修改的数据集合,使用小括号() 进行定义,元组内的元素之间使用逗号分隔。从形式上观察,除了用小括号()代替了中括号[],元组几乎与列表一样,但元组有自己独特的特性:元组一旦被创建就无法被修改,新增,删除,修改这些操作都无法在元组上进行。
2.创建元组
使用小括号,可以创建一个空的元组
t = ()
print(type(t)) # <class 'tuple'>
创建只有一个元素的元组
t = (5, )
print(type(t)) # <class 'tuple'>
即便元组里只有一个元素,也要写入一个逗号,因为小括号()既可以表示为元组的定义形式,也可以作为括号表示算数运算时的优先级,如果没有逗号,(5) 将被解析成int类型的5,在下面的表达式里,()不再被认为是元组的定义形式,而是运算符号。
>>> 5 == (5)
True
创建拥有多个元素的元组
t = ('python', 'java', 'php')
print(t) # ('python', 'java', 'php')
使用内置函数tuple创建元组
t1 = tuple(['python', 'java', 'php'])
print(t1) # ('python', 'java', 'php')
t2 = tuple('python')
print(t2) # ('p', 'y', 't', 'h', 'o', 'n')
3.访问元组里的值
获取元组里的值,方法与从列表里获取值一样,必须提供索引,通过[]索引访问的方式获得索引对应的值。
t = tuple(['python', 'java', 'php'])
print(t[0]) # python
通过for循环同样可以对元组进行遍历
t = tuple(['python', 'java', 'php'])
for item in t:
print(item)
4.元组的切片操作
元组同样支持切片操作,这方面与列表完全一致
t = tuple(['python', 'java', 'php'])
print(t[1:]) # ('java', 'php')
只要不修改元组,对列表的一切操作都可以移植到元组身上,这里不再详细讲切片,可以参考字符串一节。
5.元组真的不可以被修改么
元组是不可变对象,但有人却提出的反例
t = ([1, 2, 3], ['python'])
t[0][0] = 3
print(t)
元组t里有两个元组,两个元组都是列表,程序执行的结果是:
([3, 2, 3], ['python'])
看起来,元组似乎成功被修改了,然而这并不是事实,元组实际上没有被修改,被修改的是列表里的元素,准确的说是t[0]作为列表是可以修改的,而t没有被修改。当我们强调元组是不可变对象时,是指无法通过索引修改元组里的元素,类似下面的代码是无法被执行的
t = ([1, 2, 3], ['python'])
t[0] = '修改'
程序会报错
TypeError: 'tuple' object does not support item assignment
t[0] 是列表,我们不能将元组的第一个元素修改为其他对象,但t[0]作为列表本身是可以被修改的,但修改以后,t[0]还是原来的那个列表,只是列表里的内容发生了变化,元组t没有发生变化,仍然只有两个元素,通过内置函数id输出t[0]的内存地址能让你更清楚的认识到元组没有被修改。
t = ([1, 2, 3], ['python'])
print(id(t[0])) # 2161425011080
t[0][0] = 3
print(id(t[0])) # 2161425011080
修改前后,t[0]的内存地址不变,从元组的视角来看,它内部存储的第一个元素没有发生改变。
6.元组与列表的区别
很多人认为元组就是一个不可变的列表,虽然很形象,但并不准确。python在元组内部做了很多优化,既节省了内存又提高了性能,元组有自己特殊的使用场景,这是列表无法替代的。
6.1 函数返回多个结果
def func(x, y):
return x, y, x+y
res = func(2, 3)
print(res)
当函数有多个返回值时,最终以元组的形式返回,程序输出结果为
(2, 3, 5)
当函数返回多个结果时,以列表的形式返回,难道不也是可行的么?从程序设计的角度看,函数返回多个结果时,以元组形式返回好于以列表形式返回,原因在于列表是可变对象,这意味着函数的返回结果是可修改的,那么函数在使用时就容易出现修改函数返回值的情况。
某些情况下,我们不希望函数的返回值被他人修改,元组恰好满足了我们的要求,如果函数本意就是返回一个列表,那么在renturn时,就应该直接返回列表,而不是返回多个结果。
6.2元组作为函数的可变参数
def func(*args):
print(args, type(args))
func(3, 4, 5)
定义一个支持可变参数的函数时,args的类型是元组,在函数内可以从args中依次取出传入的参数,那么同样的问题,为什么不是列表呢?还是从程序设计的角度出发,如果args被设计成列表,由于列表可以被修改,保不齐某个程序员在实现函数的时候修改了args,传入的参数被修改了,或是增加,或是减少,这样就会引发不可预知的错误。
但现在,python将其设计成元组,元组无法修改,你在函数内部无法修改传入的参数,这就保证了函数入参的安全性。
6.3元组做字典key,存到集合中
想要成为字典的key,或是存储到集合中,必须满足可hash这个条件,所有的可变对象,诸如列表,集合,字典都不能做key,但元组可以,在一些特定情境下,用元组做key,非常实用,比如下面这个练习题目
题目要求:已知有两个列表
lst1 = [3, 5, 6, 1, 2, 4, 7]
lst2 = [6, 5, 4, 7, 3, 8]
从两个列表里各取出一个数,他们的和如果为10,则记录下来,请写程序计算,这种组合一共有几种,分别是什么,要求组合不能重复。从lst1中取3, lst2中取7,这对组合满足要求,从lst1中取7,lst2中取3,也满足要求,但是这个组合已经存在了,因此不算。使用嵌套循环,就可以轻易的完成这个题目,但是这中间过程要去除掉重复的组合,正好可以利用元组,算法实现如下
lst1 = [3, 5, 6, 1, 2, 4, 7]
lst2 = [6, 5, 4, 7, 3, 8]
res_set = set()
for i in lst1:
for j in lst2:
if i + j == 10:
if i > j:
res_set.add((j, i))
else:
res_set.add((i, j))
print(res_set)
程序输出结果
{(3, 7), (4, 6), (5, 5), (2, 8)}
去重的过程恰好利用了元组,将组合以元组的形式存储到集合中,利用集合的不重复性来达到去重的目的,元组里第一个元素是组合中较小的那个数
7.元组运算符和方法
7.1 运算符
与字符串一样,元组之间可以使用 + 号和 ***** 号进行运算。这就意味着他们可以组合和复制,运算后会生成一个新的元组。
Python 表达式 | 结果 | 描述 |
---|---|---|
len((1, 2, 3)) | 3 | 计算元素个数 |
(1, 2, 3) + (4, 5, 6) | (1, 2, 3, 4, 5, 6) | 连接 |
('Hi!',) * 4 | ('Hi!', 'Hi!', 'Hi!', 'Hi!') | 复制 |
3 in (1, 2, 3) | True | 元素是否存在 |
for x in (1, 2, 3): print (x, end=" ") | 1 2 3 | 迭代 |
7.2方法与函数
元组中的元素是不能被删掉的,不过元组也可以使用del语句,只是del语句在元组中的作用是删除整个元组,而不是删除指定的索引的值
len(tuple)、max(tuple)、min(tuple)、tuple(iterable),其中tuple()可以把可迭代的系列转换为元组,例如tuple([1,2,3,4])的结果是(1,2,3,4)
tuple没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,不可变的tuple有什么意义?因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple。tuple的陷阱:当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来
7.3列表推导式
使用小括号包裹推导式会生成生成器对象,而不是元组:
>>> a = (2*x for x in range(2))
>>> type(a)
<class 'generator'>
例如,我们可以使用下面的代码生成一个包含数字 1~9 的元组:
a = (x for x in range(1,10))
print(a)
#运行结果为:
#<generator object <genexpr> at 0x0000020BAD136620>
从上面的执行结果可以看出,使用元组推导式生成的结果并不是一个元组,而是一个生成器对象(后续会介绍),这一点和列表推导式是不同的。如果我们想要使用元组推导式获得新元组或新元组中的元素,有以下三种方式:
使用 tuple() 函数,可以直接将生成器对象转换成元组,例如:
a = (x for x in range(1,10)) print(tuple(a))
运行结果为: (1, 2, 3, 4, 5, 6, 7, 8, 9)
直接使用 for 循环遍历生成器对象,可以获得各个元素,例如:
a = (x for x in range(1,10))
for i in a:
print(i,end=' ')
print(tuple(a))
运行结果为: 1 2 3 4 5 6 7 8 9 ()
使用 next() 方法遍历生成器对象,也可以获得各个元素,例如:
a = (x for x in range(3))
print(a.__next__())
print(a.__next__())
print(a.__next__())
a = tuple(a)
print("转换后的元组:",a)
#运行结果为:
0
1
2
转换后的元组: ()
注意,无论是使用 for 循环遍历生成器对象,还是使用 next() 方法遍历生成器对象,遍历后原生成器对象将不复存在,这就是遍历后转换原生成器对象却得到空元组的原因。
8.元组的封包与解包
先看下面的代码:
a=1
b=2
a,b=b,a
print(a,b)
我们都知道这样可以很方便的对2个值进行互换,然而这个操作其实涉及到元组的封包/装包与解包/拆包
完全的写法应该是下面这样的:
(a,b)=(b,a)
将a和b放入一个元组中,然后通过元组赋值
但是python会自动进行元组的装包与拆包操作,因此下面2个式子与上面是等价的:
a,b=(b,a)
(a,b)=b,a
理解了元组的自动装包拆包,再回头看函数的返回值,就可以更深入的理解了
函数其实并不能返回多个值,只能返回一个值。
当有多个返回值时,其实是自动将他们放入一个元组中,然后返回这个元组
def f():
return 1,2,3
print(f())
此时函数返回值其实是(1,2,3),是个元组
但是当我们用3个变量同时去接收这个返回值时
a,b,c=f()
相当于
a,b,c=(1,2,3)
由于元组自动拆包,造成a=1,b=2,c=3,看似返回了多个值一样
如果不理解这一点,就会搞不清为什么有时候就有括号,有时候就没括号
关键有括号和没括号类型完全不一样,搞混了可是不行的
9. python的封包和解包
9.1.封包:自动封包成元组
将多个值赋值给一个变量时,python会自动将这些值封装成元组,这个特性称之为封包
a = 1, 2, 3
print(a, type(a)) # (1, 2, 3) <class 'tuple'>
当函数返回多个数值时,也会进行封包
def test():
return 1, 2, 3
a = test()
print(a, type(a)) # (1, 2, 3) <class 'tuple'>
实践中,封包操作无处不在,但我们很少很少主动使用封包操作,都是Python自动完成
python解包可以将元组解包成可变参数,将字典解包成关键字参数,下面列列举几种使用python解包的场景
9.1 解包:接收函数返回值
def test():
return 1, 2, 3
a, b, c = test()
print(a, b, c) # 1 2 3
函数的返回值是一个元组,左侧是三个变量,这样就会发生解包,a, b, c依次等于元组里的元素,函数的返回值有3个,被封包成了元组, 赋值语句的左侧不一定非得是3个变量
def test():
return 1, 2, 3
a, *b = test()
print(a, b) # 1 [2, 3]
变量a赋值为1, 变量b前面有一个星号,剩余的2, 3 将被解包为列表
9.2 解包:遍历字典
my_dic = {
'一': 1,
'二': 2,
'三': 3
}
for item in my_dic.items():
print(item)
# 解包
for key, value in my_dic.items():
print(key, value)
9.3 解包:传递参数
def func(*args):
print(sum(args))
a = (2, 4, 6)
func(*a) # 将元组解包成可变参数
def func_2(**kwargs):
for key, value in kwargs.items():
print(key, value)
b = {'一': 1, '二': 2}
func_2(**b) # 将字典解包成关键字参数
解包技术在实践中大量应用,比如使用python操作redis时,如果你想一次性向集合中添加多个值,就必须使用解包结束传入参数
import redis
from conf.redis_conf import RedisConfig, QueueConfig
r = redis.Redis(host=RedisConfig.host, port=RedisConfig.port,
password=RedisConfig.password, db=RedisConfig.db)
tup = ('apple', '谷歌', '阿里', '腾讯')
r.sadd('my_set', *tup)
sadd的方法定义如下
def sadd(self, name, *values):
"Add ``value(s)`` to set ``name``"
return self.execute_command('SADD', name, *values)
如果不使用解包技术,就只能在调用sadd方法时手动逐个写入参数,耗时又费力
9.4 解包:合并两个字典
巧妙的利用解包技术,可以简单方便的将两个字典合并到一个新字典中
dic_1 = {'一': 1}
dic_2 = {'二': 2}
dic_3 = {**dic_1, **dic_2}
print(dic_3) # {'一': 1, '二': 2}