• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Python数据类型:字符串、列表、元组

Python编程 彭东稳 7年前 (2017-08-01) 18064次浏览 已收录 0个评论

一、序列

在Python中,最基本的数据结构是序列(sequence)。序列中的每个元素被分配一个序号——即元素的位置,也称为索引。第一个索引是 0,第二个则是 1,以此类推。序列中的最后一个元素标记为 -1,倒数第二个元素为 -2,以次类推。

所有序列类型都可以进行某些特定的操作。这些操作包括:索引(indexing)、分片(sliceing)、加(adding)、乘(multiplying)以及检查某个元素是否属于序列的成员(成员资格)。除此之外,Python还有计算序列长度、找出最大元素和最小元素的内建函数等等。另外所有序列都支持迭代,统称为可迭代对象(iterable)。

Python包含多种内建的序列,本文主要说列表、元组和字符串。其中字符和元组属于不可变序列,不能更改,代码更安全;列表则支持插入,删除和替换等元素,是一种可变的序列。

二、通用序列操作(字符串、列表、元组)

  • 索引运算:key[index]

Python索引是从0开始的,当索引超出了范围时,Python会报一个IndexError错误。所以,要确保索引不要越界,记得最后一个元素的索引是len(classmates) – 1,如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素。切片操作对列表来说是使用非常广泛的,灵活性特别大。

  • 切片运算:key[index_start:index_end]

切片之后创建的是一个新的内存对象

  • 扩展切片运算:key[index_start:index_end:stride]

  • 相关函数(按照其在ASCII码中的顺序)

  • 成员关系判断:OBJ in NAME

三、字符串(string)

字符串是Python中最常用的数据类型之一,使用单引号或双引号来创建字符串,使用三引号创建多行字符串。字符串要么使用两个单引号,要么两个双引号,不能一单一双!Python不支持单字符类型,单字符在Python中也是作为一个字符串使用。

字符串是不可变的序列数据类型,不能直接修改字符串本身,和数字类型一样!

Python 2字符串不支持Unicode编码,其大小为8bit,要想支持Unicode编码,需使用方法u”content”。而在Python 3全面支持Unicode编码,所有的字符串都是Unicode字符串,不在需要使用u,可以自动识别,其大小为16bit。所以传统Python 2存在的编码问题不再困扰我们,可以放心大胆的使用中文。

特别注意,Python 的字符串是不可变的(immutable)。因此,用下面的操作,来改变一个字符串内部的字符是错误的。不允许的。

Python 中字符串的改变,通常只能通过创建新的字符串来完成。比如上述例子中,想把 ‘hello’ 的第一个字符 ‘h’,改为大写的 ‘H’,我们可以采用下面的做法:

第一种方法,是直接用大写的 ‘H’,通过加号操作符,与原字符串切片操作的子字符串拼接而成新的字符串。

第二种方法,是直接扫描原字符串,把小写的 ‘h’ 替换成大写的 ‘H’,得到新的字符串。

由此,可以看出想改变字符串必须得老老实实创建新的字符串。因此,每次想要改变字符串,往往需要 O(n) 的时间复杂度。其中,n 为新字符串的长度。但随着 Python 版本的更新,Python 也越来越聪明了,性能优化得越来越好了。

这里,着重说一下,使用加法操作符 ‘+=’ 的字符串拼接方法。因为它是一个例外,打破了字符串不可变的特性。操作方法如下所示:

来看下面的例子:

这个例子,每次循环,似乎都得创建一个新的字符串;而每次创建一个新的字符串,都需要 O(n) 的时间复杂度。因此,总的时间复杂度为 O(1) + O(2) + … + O(n) = O(n^2)。这样到底对不对呢?在 Python2.5 之前,确实是这样的。但自从 Python 2.5 开始,每次处理字符串的拼接操作时,Python 首先会检测 str1 还有没有其他的引用。如果没有的话,就会尝试原地扩充字符串 buffer 的大小,而不是重新分配一块内存来创建新的字符串并拷贝。这样的话,上述例子中的时间复杂度就仅为 O(n) 了。

因此,以后在写程序遇到字符串拼接时,如果使用 ‘+=’ 更方便,就放心地去用吧,不用过分担心效率问题了。

字符常用的内置方法

str.upper()

将一个字符串转变为大写。

str.lower()

将一个字符串转变为小写。

str.capitalize()

将一个字符串首字母转换我大写。

str.strip([chars])

返回去除两侧(不包括内部)指定字符串,默认是去除空格;另外还有rstrip和lstrip,分别是删除右边和左边指定字符。

str.index(sub[, start[, end]])

找到指定字符串首次出现的位置,[, start[, end]]表示从哪里开始和结束,可省略。

str.replace(old, new[, count])

替换一个字符或一个字符串,其中old表示修改前内容,new表示修改后内容,count表示要修改几个,可省略。

str.split(sep=None, maxsplit=-1)

用来将字符串分割成序列,可以执行最大分割多少次。

配置索引运算,可以显示执行的元素:

str.startswith(suffix[, start[, end]])

判断对象中是否为执行字符首部,是则为真,否则为假。

str.endswith(suffix[, start[, end]])

判断对象中是否为执行字符结尾,是则为真,否则为假。

str.join(iterable)

使用’某某’作为分隔符连接序列中的字符。

str.find(sub[, start[, end]])

可以在一个较长的字符串中查找子串,它返回子串所在位置的最左端索引,如果没有找到则返回-1。

str.translate(table)

这个方法和replace方法一样,可以替换字符串中的某些部分,但是和前者不同的是,translate方法只处理单个字符。它的优势在于可以同时进行多个替换,有些时候比replace效率高的多。

str.format()

格式化输出字符串。简单的说就是format里面的东西去替换前面的内容,在替换的时候,可以按某种规定来输出;format方法在Python2.6之后引入,替代了原先的%,显得更加优雅。

str.isalnum()

是否全为字母或数字。

str.isalpha()

是否为全字母。

str.isdigit()

是否为全数字。

str.islower()

是否为全小写。

str.isupper()

是否为全大写。

str.isspace()

是否为空格。

str.isdecimal()

是否为小数。

还有很多方法,如: center()、decode()、encode()、rindex()、rsplit()等。

四、列表(list)

列表是 Python 中最基本也是最常用的数据结构之一。列表中的每个元素都被分配一个数字作为索引,用来表示该元素在列表内所排在的位置。第一个元素的索引是0,第二个索引是1,依此类推。实际上,列表和元组,都是一个可以放置任意数据类型的有序集合。在绝大多数编程语言中,集合的数据类型必须一致。不过,对于 Python 的列表和元组来说,并无此要求。

Python的列表属于容器类型,是一个有序可重复的元素集合,可嵌套、迭代、修改、分片、追加、删除,成员判断等操作。

内置函数del()可删除元素(删除元素/删除切片/删除扩展切片)

列表常用的内置方法

L.append(object)

追加元素到末尾,一次只能追加一个元素。

L.insert(index, object)

指定索引位置添加元素。

L.extend(iterable)

合并列表元素,跟append方法作用是不同的,可以对比一下代码。

L.count(value)

查询指定元素出现的次数。

L.index(value, [start, [stop]])

查看列表中指定字符出现的索引位置。

把索引使用变量替代可以做更加方便的原处修改

L.pop([index])

根据索引从列表中剔除一个元素,不给索引默认剔除最右边的一个元素,超出索引范围会抛出异常。

L.remove(value)

根据对象删除一个元素。

L.sort(key=None, reverse=False)

根据元素排序,同样是在列表原处修改,支持升序降序。

L.reverse()

根据元素反转,同样是在列表原处修改。

PS:Python 3针对list增加了clear、copy方法。

五、元组(tuple)

元组跟列表非常类似,属于容器类型,是任意对象的有序集合。但是tuple一旦初始化就不能修改(immutable),它属于不可变对象。

现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。

不可变的tuple有什么意义?因为tuple不可变,所以性能更优(指定全部元素为immutable的tuple会作为常量在编译时确定,所以速度更快),多线程安全,不需要锁,不担心被恶意修改或者不小心修改。如果可能,能用tuple代替list就尽量用tuple。

另外就是元组可哈希(字符也可哈希),列表不可哈希(字典和集合也不可哈希),什么意思呢?简单理解就是支持hash()函数的称之为可哈希,反之称之为不可哈希。

由于tuple是immutable对象,可哈希,所以可以当做dict的key;而list不可哈希,自然也不能当做dict的key。如下测试。

在Python的内置数据类型中,基本上可变的数据类型都是不可哈希的,而不可变的数据类型是可哈希的。

元组的初始化操作:

元组嵌套列表,虽然元组本身不可变,但是如果元组内嵌套了可变类型的元素,那么此类元素的修改不会返回新元素而是在原处修改。

表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变,指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的。理解了“指向不变”后,要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。

元组常用的内置方法

由于元组不可变,所以相对应的增删改方法都没有,只有count和index方法。

T.count(value)

统计元素出现的次数。

T.index(value, [start, [stop]])

显示元素的索引位置。

六、说说不可变对象

上面我们讲了,str和tuple是不可变对象,而list是可变对象。在Python的内置数据类型中,基本上可变的数据类型都是不可哈希的,而不可变的数据类型是可哈希的。对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:

而对于不可变对象,比如str,对str进行操作呢:

使用字符串有个replace()方法,也确实变出了’Abc’,但变量a最后仍是’abc’,这就是不可变对象,应该怎么理解呢? 我们a.replace(‘a’,’A’)结果赋值给一个变量:

要始终牢记的是,a是变量,而’abc’才是字符串对象!

当我们调用a.replace(‘a’, ‘A’)时,实际上调用方法replace是作用在字符串对象’abc’上的,而这个方法虽然名字叫replace,但却没有改变字符串’abc’的内容。相反,replace方法创建了一个新字符串’Abc’并返回,如果我们用变量b指向该新字符串,就容易理解了,变量a仍指向原有的字符串’abc’,但变量b却指向新字符串’Abc’了。

所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。

为了更好的说明问题,我们来看一个小实例,再来理解一下什么是可变和不可变对象。

执行程序,结果如下:

str为不可变对象,当一个引用a传递给函数fun_a的时候,函数会自动复制一份引用a(可以参考打印输出的id内存地址),这个函数里的引用a和外边的引用a没有半毛关系,内存地址就不一样!也就是说函数fun_a把引用指向了一个不可变对象nums、str,所以不会影响到函数外面的同名变量。

执行程序,结果如下:

list为可变对象,函数内的引用指向的是可变对象list,对它的操作就和定位了指针地址一样,在内存里进行修改。可以看到对list进行append操作内存地址无论是在函数内还是函数外都是不变的!

七、列表与元祖的存储方式

前面说了,列表和元祖最重要的区别就是,列表是动态的,可变的;而元祖是静态的,不可变的。这样的差异,势必会影响两者的存储方式。来看下面的例子:

可以看到,对列表和元祖,我们放置了相同的元素,但是元祖的存储空间,却比列表要少 16 字节。这是为什么呢?

事实上,由于列表是动态的,所以它需要存储指针,来指向对应的元素(上述列子中,对于 int 类型,8 字节)。另外,由于列表可变,所以需要额外存储已经分配的长度大小(8字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外的空间。

上面的例子,大概描述了列表空间分配的过程。我们可以看到,为了减小每次增加/删除操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效:增加/删除的时间复杂度均为 O(1)。

但是对于元祖,情况就不同了。元祖长度大小固定,元素不可变,所以存储空间固定。

八、列表和元祖的性能

通过上面列表和元祖存储方式的差异,我们可以得出结论:元祖要比列表更加轻量级一些,所以总体上来说,元祖的性能速度要略优于列表。

另外,Python 在后台,对静态数据做一些资源缓存。通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。

但是对于一些静态变量,比如元祖,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。这样,下次我们再创建同样大小的元祖时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。

下面的例子,是计算初始化一个相同元素的列表和元祖分别所需的时间。我们可以看到,元祖的初始化速度,要比列表快 5 倍。

但如果是索引操作的话,两者的速度差别非常小,几乎可以忽略不计。

当然,如果你想要增加、删除或者改变元素,那么列表显然更优。原因你现在肯定知道了,那就是对于元祖,你必须得通过新建一个元祖来完成。

Tips:列表和元祖的内部实现都是 array 的形式,列表因为可变,所以是一个 over-allocate 的 array,元祖因为不可变,所以长度大小固定。

<延伸>

Python解包(Unpacking)

Python的tuple是不是冗余设计?


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!