一、元组(tuple)数据类型讲解

1.元组是什么

python的元组是有序且不可被修改的数据集合,使用小括号() 进行定义,元组内的元素之间使用逗号分隔。从形式上观察,除了用小括号()代替了中括号[],元组几乎与列表一样,但元组有自己独特的特性:元组一旦被创建就无法被修改,新增,删除,修改这些操作都无法在元组上进行。

image-20221001204633997

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)

image-20221001204734135

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}