字符串的创建与驻留机制

  • 字符串的多种创建方法
  • 字符串的驻留机制

字符串的多种创建方法:

a1 = 'python'
a2 = "python"
a3 = '''python'''
a4 = """python"""
a5 = str(123456)
print(a1, type(a1))
print(a2, type(a2))
print(a3, type(a3))
print(a4, type(a4))
print(a5, type(a5))

运行结果:

  • python <class 'str'>
  • python <class 'str'>
  • python <class 'str'>
  • python <class 'str'>
  • 123456 <class 'str'>

字符串总共分为这五种创建方式,单引号,双引号,单三引号,双三引号还有直接用内置函数str()把数据转换成字符串

字符串的驻留机制:

值相同的字符串符合一定的条件时,在内存中只保留一份拷贝,在需要调用时直接赋值给新的变量,驻留机制可以避免频繁的创建和删除,节省空间和提升运行速度

a1 = 'python'
a2 = "python"
a3 = '''python'''
a4 = 'pyt' + 'hon'
print(a1, id(a1))
print(a2, id(a2))
print(a3, id(a3))
print(a4, id(a4))

运行结果:

  • python 31700784
  • python 31700784
  • python 31700784
  • python 31700784

结论可以看出,无论是用什么方法创建的字符串,甚至用拼接符拼接的字符串,只要得到的字符串值相同,ID标识也就相同,这就是字符串的驻留,一个字符串ID同时赋值给四个不同的变量

字符串的驻留条件:(需要在电脑自带的交互模式CMD中验证,解释器一般都优化了验证不出来)

  • 字符串的长度为01
  • 符合标识符的字符串
  • 字符串只在编译时驻留,而非运行时
  • -5256之间的整数数字

第一点和第二点一起解析,看代码

a = ''
b = ''  # 字符串长度为0(空字符串)
print(a is b)
a = '%'
b = '%'  # 字符串长度为1
print(a is b)
a = '%%'
b = '%%'  # 字符串长度大于1
print(a is b)

运行结果:

  • True
  • True
  • False

首先要明白符合标识符的字符串是什么,就是只有字母,数字和下划线_的字符串,文字也算做字母,代码中用比较运算符is来比较两个字符串的ID是不是一样的,结果可以看出,当字符串长度为0或者1的时候,不管是不是符合标识符的字符串,都可以驻留,所以ID相同,结果为True,而当字符串长度大于1的时候,就需要符合标识符的字符串才可以驻留,所以ID不相同,结果为False

字符串只在编译时驻留,而非运行时

a = 'abc'
b = 'ab' + 'c'
c = ''.join(['ab', 'c'])
print(a, type(a), id(a))
print(b, type(b), id(b))
print(c, type(c), id(c))

运行结果:

  • abc <class 'str'> 31244656
  • abc <class 'str'> 31244656
  • abc <class 'str'> 31701104

Python的编译器在我们输入代码的时候就已经在编译了,这个时候字符串的驻留已经完成,例如字符串'ab'用连接符+号连接字符串'c',其实在输入后就已经编译成了字符串'abc',然后发现驻留池中有相同的字符串,自然不会创建新的字符串,变量c中的join()函数也是把列表里面的两个字符串'ab''c'连接,不同的是join()函数是在运行的时候再进行连接生成一个新的字符串,这样的好处是字符串'ab''c'无需编译,始终只操作一个新的字符串,而用+号连接两个字符串的方式需要编译两个子串,然后看驻留池中有没有相同的字符串,有再拿出来赋值,这样就操作了三个字符串,所以join()函数的方式要比+号连接的方式节省空间和运行速度

如果join()函数括号内只有一个字符串,无需连接,那么也会在驻留池内找相同的字符串赋值,不再生成新的字符串,不过如果不需要连接的话,其实就没必要用join()函数了

a = 'abc'
b = 'ab' + 'c'
c = ''.join(['abc'])
print(a, type(a), id(a))
print(b, type(b), id(b))
print(c, type(c), id(c))

运行结果:

  • abc <class 'str'> 37732784
  • abc <class 'str'> 37732784
  • abc <class 'str'> 37732784

-5256之间的整数数字:

a = -5
b = -5
print(a is b)
a = -6
b = -6
print(a is b)

运行结果:

  • True
  • False

这个很好理解,这个区间内的整数数字可以驻留,超过范围不驻留,是整数类型,不是字符串类型

调用sys模块可以强制字符串驻留

a = '%%'
b = '%%'
print(a is b)
import sys
a = sys.intern(b)
print(a is b)

运行结果:

  • False
  • True

后面会学到详细的相关知识,按上述代码的写法,先调用模块import sys,然后a = sys.intern(b),强制字符串ab驻留(原字符串是长度大于1不符合标识符的字符串),所以最后字符串的ID相同,比较结果为True

字符串的查询操作

字符串的查询和列表一样按序号查询,一样的正序序号和倒序序号排列

  • index() 查询子串(substr)第一次出现的位置,有则返回子串第一个字符对应的序号,没有则报错
  • rindex() 查询子串(substr)最后一次出现的位置,有则返回子串第一个字符对应的序号,没有则报错
  • find() 查询子串(substr)第一次出现的位置,有则返回子串第一个字符对应的序号,没有则返回-1
  • rfind() 查询子串(substr)最后一次出现的位置,有则返回子串第一个字符对应的序号,没有则返回-1

index()rindex()

a = 'python,python,python'  # 字符串长度为20,正序序号从左到右为0到19
b = 'python'
print(a.index('on'))
print(a.rindex('on'))
print(b.rindex('on'))

运行结果:

  • 4
  • 18
  • 4

字符的排序对应的序号看之前列表的笔记,不再解释,字符串变量a中有三个重复的子串'on',子串第一次出现的位置为子串第一个字符也就是'o'从左到右对应的正序序号为4,最后一次出现的位置是从左到右第三个'python'中的'on',对应的正序序号为18,字符串变量b中只有一个子串'on',所以第一次出现的位置也就是最后一次出现的位置,对应的正序序号为4

find()rfind()

a = 'python,python,python'
print(a.find('on'))
print(a.rfind('on'))
print(a.find('kk'))

运行结果:

  • 4
  • 18
  • -1

index()函数不同的是,find()函数查询的子串不存在的话,返回值为-1,而不是报错,其它相同,建议使用find()rfind()函数,不会报错

字符串的大小写转换操作

  • upper() 把字符串所有字母转换为大写字母
  • lower() 把字符串所有字母转换成小写字母
  • swapcase() 把字符串小写字母转大写字母,大写字母转小写字母
  • capitalize() 把字符串第一个字符转换为大写字母,其它转换为小写字母,如果第一个字符不是字母则无变化
  • title() 把字符串每一段字母串的第一个字母转换为大写字母,其它转换为小写字母(一段连续字母串,可以用空格,符号,数字等隔开来形成几段字母串)
a = 'python,Python,Pyt6hon'
print(a)
print(a.upper())
print(a.lower())
print(a.swapcase())
print(a.capitalize())
print(a.title())

运行结果:

  • python,Python,Pyt6hon
  • PYTHON,PYTHON,PYT6HON
  • python,python,pyt6hon
  • PYTHON,pYTHON,pYT6HON
  • Python,python,pyt6hon
  • Python,Python,Pyt6Hon

转换之后无论是否与原字符串值相同,标识ID都会改变,也就是新建了一个字符串

字符串内容的对齐操作

  • center() 居中对齐,接受两个参数,第一个参数是行宽度,行宽度小于等于字符串宽度则返回原字符串,第二个参数是填充符,不写第二个参数则默认用空格做填充符
  • ljust() 左对齐,接受两个参数,第一个参数是行宽度,行宽度小于等于字符串宽度则返回原字符串,第二个参数是填充符,不写第二个参数则默认用空格做填充符
  • rjust() 右对齐,接受两个参数,第一个参数是行宽度,行宽度小于等于字符串宽度则返回原字符串,第二个参数是填充符,不写第二个参数则默认用空格做填充符
  • zfill() 右对齐,接受一个参数,也就是行宽度,行宽度小于等于字符串宽度则返回原字符串,默认用数字0做填充符,不接受指定
a = 'python'
print(a)
print(a.center(10))
print(a.center(10, '-'))
print(a.ljust(10, '-'))
print(a.rjust(10, '-'))
print(a.zfill(10))

运行结果:

  • python
  • ^^python^^(这里用^表示空格)
  • --python--
  • python----
  • ----python
  • 0000python

第二个参数要用长度为1的字符串指定

当字符串第一个字符为符号'-'负号的时候

b = '-hello'
print(b.rjust(10, '+'))
print(b.zfill(10))

运行结果:

  • ++++-hello
  • -0000hello

从运行结果可以看出,rjust()对齐的结果跟之前一样,指定的填充符都在最左侧,整体为右对齐,而zfill()对齐的结果则是'-'负号在最左侧,填充符数字0'-'负号和其它字符之间,整体向右对齐,整体的行宽度没有超过指定的行宽度,测试后发现,这种情况只有最左侧为'-'负号且用zfill()右对齐的情况下才生效

字符串的劈分操作

  • split() 从字符串左边开始劈分,默认是把空格字符当作劈分点,返回值是列表
  • rsplit() 从字符串右边开始劈分,默认是把空格字符当作劈分点,返回值是列表
a = 'python hello Word'
b = 'python|hello|Word'
print(a.split())
print(a.rsplit())
print(b.split())
print(b.rsplit())
print(b.split(sep='|'))
print(b.rsplit(sep='|'))
print(b.split(sep='|', maxsplit=1))
print(b.rsplit(sep='|', maxsplit=1))

运行结果:

  • ['python', 'hello', 'Word']
  • ['python', 'hello', 'Word']
  • ['python|hello|Word']
  • ['python|hello|Word']
  • ['python', 'hello', 'Word']
  • ['python', 'hello', 'Word']
  • ['python', 'hello|Word']
  • ['python|hello', 'Word']

看上面代码,先声明了两个字符串变量,一个用空格把字符隔开,一个用竖线把字符隔开,当然无论是空格还是竖线本身也属于字符,split()rsplit()括号里面可以接受两个参数,一个参数sep=后面填写的是作为劈分点的字符,如果不填写sep=参数,默认用空格字符作为劈分点,第五行第六行代码没有填写sep=参数,默认把空格字符作为劈分点,但字符串变量b没有空格字符,所以整个字符串被作为一个整体用列表输出了,另一个参数maxsplit=后面填写的是劈分次数,例如字符串变量b有两个竖线字符把字符串分隔成三段,所以需要两次劈分才能完整的把三个字符串劈分出来,这个时候设置劈分次数为1,也就是只劈分一次,剩下字符的连同设定的作为劈分点的字符一起作为整体一个字符串输出,split()rsplit()在没有设定劈分次数或者设定的劈分次数大于等于总可劈分次数的时候,是看不出区别的,只有设定的劈分次数小于总可劈分次数的时候,才能发现确实split()是从左开始劈分的,而rsplit()是从右开始的,这些都可以从上面代码对应的运行结果看出来

判断字符串的操作

  • isidentifier() 判断字符串是否由合法的标识符组成
  • isspace() 判断字符串是否由空白字符组成(回的,换行,水平制表符)
  • isalpha() 判断字符串是否由字母组成
  • isdecimal() 判断字符串是否由十进制数字组成
  • isnumeric() 判断字符串是否由数字组成
  • isalnum() 判断字符串是否由字母和数字组成
a1 = '张三Python_1Ⅱ3四'  # 只含有字母数字下划线的字符串(文字被识别为字母,阿拉伯数字,罗马数字,中文数字等都被识别为数字)
print(a1.isidentifier())
a2 = '\n\r\t'  # 换行符,回的符,水平制表符都是空白字符(看之前的转义字符笔记)
print(a2.isspace())
a3 = '张三abc'  # 文字被识别为字母
print(a3.isalpha())
a4 = '123'  # 只有最常用的阿拉伯数字被识别为十进制的数字
print(a4.isdecimal())
a5 = '1Ⅱ3四'  # 阿拉伯数字,罗马数字,中文数字等,都被识别为数字
print(a5.isnumeric())
a6 = '张三abc1Ⅱ3四'  # 字母(文字被识别为字母)或者数字(也可以是中文数字甚至罗马数字等)或者字母加数字组成都可以
print(a6.isalnum())

运行结果:

  • True
  • True
  • True
  • True
  • True
  • True

上面的代码注释已经很好的解析了,运行返回值True说明是对的

字符串的其它操作

  • replace() 字符串替换,可以接受三个参数,第一个参数是被替换的子串,第二个参数是替换子串的字符串,第三个参数是最大替换次数(不设定会把所有与设定的被替换的子串相同的子串全部替换掉,设定后会按从左到右的顺序一个个替换直到达到设定的替换次数)
  • join() 将列表或者元组中的字符串合并成一个字符串(运行时合并,新建字符串,不会从驻留池拿相同的字符串来赋值)

replace()

a = 'python,python,python'
print(a.replace('on', 'ee', 2))

运行结果:

  • pythee,pythee,python

设定的被替换的子串是'on',设定的替换子串的字符串是'ee',设定的最大替换次数是2次,看运行结果,字符串中的三个子串'on'被从左到右替换了两个,符合设定的替换次数和替换顺序

join()

print(''.join(['张三', 'python', '李四']))
print('+'.join(['张三', 'python', '李四']))
print('+'.join('python'))

运行结果:

  • 张三python李四
  • 张三+python+李四
  • p+y+t+h+o+n

第一行代码,原字符串是空字符串,列表中的三个字符串直接拼接到了一起,第二行代码,原字符串有一个字符'+'号,列表中的三个字符串在拼接的时候把原字符串分别插入到了两个拼接处进行拼接,第三行代码,原字符串有一个字符'+'号,join()括号里面的是一个字符串序列,系统认定字符串序列每一个字符都是单独的,所以拼接的时候在每两个字符中间都插入了原字符串进行拼接

字符串的比较操作

使用比较运算符可以对两个字符串进行比较(>,>=,<,<=,==,!=

比较规则:首先比较两个字符串中的第一个字符,如果相等则继续比较下一个字符,依次比较下去,直到两个字符串中的字符不相等或者再没有字符可以比较时,其比较结果就是两个字符串的比较结果,得到比较结果后,两个字符串中的所有后续字符都将不再被比较

比较原理:两个字符进行比较时,比较的是其原始值ordinal value,调用内置函数ord()可以得到指定字符的原始值,调用内置函数chr()可以获得指定原始值对应的字符

a = '123456'
b = '123457'
c = '1234567'
print(a > b)
print(b > a)
print(b > c)
print(a < c)

运行结果:

  • False
  • True
  • True
  • True

用数字字符串是方便观察,其实任何字符都是可以比较的,字符串a小于字符串b可以很直观的看出来,不做解析,字符串b6个字符,字符串c7个字符,为什么字符串b大于字符串c呢?因为比较到第6个字符的时候,字符串b里面的字符比较大,直接获得了比较结果,后续的字符不再比较,字符串a和字符串c的前6个字符都是一样的,但是字符串c多了一个字符,所以字符串c一定大于字符串a

下面看下字符的原始值

print(ord('6'))
print(ord('7'))
print(chr(54))
print(chr(55))

运行结果:

  • 54
  • 55
  • 6
  • 7

用函数ord()获取字符的原始值,用函数chr()获取原始值对应的字符

字符串的切片操作

字符串切片和列表切片的写法是一样的,详细可以查看之前的列表切片笔记,原理一样的

a = 'python,java'
print(a[0:6:1])

运行结果:

  • python

原理同列表切片,不在解析

格式化字符串

什么叫格式化字符串?我们平常需要做一些登记表格,这个时候大部分内容是无需改动的,一些姓名年龄之类的信息是需要频繁改动的,这个时候,我们就需要用到格式化字符串,也就是用占位符占据需要修改的数据位,后面只需要修改这些占位符对应的数据就可以了

格式一:'字符字%s符字符字符%i字符字符' % (数据1, 数据2)

%百分号做占位符,%s对应的是字符串数据类型,%i或者%d对应的是整数数据类型,%f对应的是浮点数数据类型,字符串中第一个占位符只能调用数据1,第二个占位符只能调用数据2,以此类推,所以有多少占位符就要有多少个对应的数据以供调用(占位符和数据的数量要对等),整数和浮点数类型的数据可以用%s的字符串占位符调用,但带有文字字母或者符号的字符串数据不能用%i%d或者%f的整数浮点数占位符调用,因为数据类型转换不了(跟之前学的数据类型转换原理想通)

格式二:'字符字{0}符字符字符{1}字符{0}字符'.format(数据0, 数据1)

{}花括号做占位符,花括号内的序号从0开始代表调用第几个数据,0表示调用数据01表示调用数据1,方便有些需要调用重复数据的字符串使用,不限制数据类型,可以随意调用(占位符和数据的数量可以不对等,只要保证所有占位符都调用了数据)

a = '张三'
b = 20
c = 98.7
d = 28
print('我的名字叫:%s,今年%i岁了,我考了%f分,上学的日子是%d号' % (a, b, c, d))
print('我的名字叫:{0},今年{1}岁了,我考了{2}分,我真的叫:{0}'.format(a, b, c))

运行结果:

  • 我的名字叫:张三,今年20岁了,我考了98.700000分,上学的日子是28号
  • 我的名字叫:张三,今年20岁了,我考了98.7分,我真的叫:张三

用百分号%做占位符的方法我们可以看到浮点数后面默认保留了6位小数,这个时候我们只需要把浮点数占位符%f改成%.1f,这样就可以只保留1位小数,要保留3位小数就改成%.3f,以此类推

a = '张三'
b = 20
c = 98.7
d = 28
print('我的名字叫:%s,今年%i岁了,我考了%.1f分,上学的日子是%d号' % (a, b, c, d))

运行结果:

  • 我的名字叫:张三,今年20岁了,我考了98.7分,上学的日子是28号

还有一种常用的格式化字符串方法

格式三:f'字符字{数据1}符字符字符{数据2}字符字符'

也是用{}花括号做占位符,但和格式二不同的是花括号内填写的是直接的数据而不是调用数据的序号,需要在字符串前面加f才能生效

a = '张三'
b = 20
print(f'我的名字叫:{a},今年{b}岁了')

运行结果:

  • 我的名字叫:张三,今年20岁了

百分号%占位符的宽度和精度

print('%d' % 99)
print('%10d' % 99)
print('%f' % 3.1415926)
print('%.3f' % 3.1415926)
print('%10.3f' % 3.1415926)

运行结果:(下面用-代表空格)

  • 99
  • --------99
  • 3.141593(PS:默认保留6位小数)
  • 3.142
  • -----3.142

当字符串内只有一个百分号%占位符的时候,后面也就只需要一个数据,所以不需要括号包裹也可以,从上面代码和运行结果可以看出,在占位符%和代表数据类型的字母d中间加一个整数就调整了这个占位符输出数据后的宽度,数据99的宽度为2,输出的总宽度为10,所以输出后99前面有8个空格,跟前面学的右对齐有点像,但不能用别的字符代替空格,而且只能是右对齐,把这个整数换成.N,这个N是整数几,后面的小数就保留几位,.3保留3位,.1保留1位,.0不保留小数等等,也可以直接在控制宽度的整数后面加上控制精度的.N,这样就实现了宽度和精度的同时控制,最后一行代码输出后前面只有5个空格是因为小数点也算1个宽度,小数被裁掉的部分第一个数字会四舍五入进到保留的部分最后一位

如果我们想更好的控制占位符输出后的宽度和对齐,可以直接在数据的源头使用前面学过的字符串对齐操作,看下面代码

a = '张三'.center(4, '+')
b = 20
c = 98.7
d = 28
print('我的名字叫:%s,今年%i岁了,我考了%.1f分,上学的日子是%d号' % (a, b, c, d))
print('我的名字叫:{0},今年{1}岁了,我考了{2}分,我真的叫:{0}'.format(a, b, c))
print(f'我的名字叫:{a},今年{b}岁了')

运行结果:

  • 我的名字叫:+张三+,今年20岁了,我考了98.7分,上学的日子是28号
  • 我的名字叫:+张三+,今年20岁了,我考了98.7分,我真的叫:+张三+
  • 我的名字叫:+张三+,今年20岁了

还有下面这种方法

a = '张三'
b = 20
c = 98.7
d = 28
print('我的名字叫:%s,今年%i岁了,我考了%.1f分,上学的日子是%d号' % (a.center(4, '+'), b, c, d))
print('我的名字叫:{0},今年{1}岁了,我考了{2}分,我真的叫:{0}'.format(a.center(4, '-'), b, c))
print(f'我的名字叫:{a},今年{b}岁了')

运行结果:

  • 我的名字叫:+张三+,今年20岁了,我考了98.7分,上学的日子是28号
  • 我的名字叫:-张三-,今年20岁了,我考了98.7分,我真的叫:-张三-
  • 我的名字叫:张三,今年20岁了

用哪种方式就看你想要控制整体还是局部数据对齐了,要是字符串数据类型才可以这样操作,如果不是的话,需要先转换数据类型

花括号{}占位符的宽度和精度

print('{}'.format(3.1415926))  # 只有一个占位符的情况下,如果不填写序号,默认就是序号0
print('{0}'.format(3.1415926))  # 为方便演示还是加上序号0
print('{0:.3}'.format(3.1415926))  # 序号0后面加:冒号然后加.3,代表整数和小数不算小数点的宽度为3
print('{0:.3f}'.format(3.1415926))  # .3f才是代表小数保留3位
print('{0:10}'.format(3.1415926))  # :冒号后面整数10代表数据输出宽度
print('{0:10.3f}'.format(3.1415926))  # 同时设定宽度和小数精度

运行结果:(下面用-代表空格)

  • 3.1415926
  • 3.1415926
  • 3.14
  • 3.142
  • -3.1415926
  • -----3.142

这里小数默认保留位数不同的是,有多少位保留多少位,这是因为花括号占位符没有明确指明数据的类型,整体原理跟百分号%占位符的宽度和精度控制差不多

字符串的编码转换

  • GBK 编码方式的一种,一个中文占两个字节
  • UTF-8 编码方式的一种,一个中文占三个字节
a = '天涯共此时'
print(a.encode(encoding='GBK'))  # 编码
b = a.encode(encoding='GBK')
print(b.decode(encoding='GBK'))  # 解码
print(a.encode(encoding='UTF-8'))  # 编码
c = a.encode(encoding='UTF-8')
print(c.decode(encoding='UTF-8'))  # 解码

运行结果:

  • b'\xcc\xec\xd1\xc4\xb9\xb2\xb4\xcb\xca\xb1'
  • 天涯共此时
  • b'\xe5\xa4\xa9\xe6\xb6\xaf\xe5\x85\xb1\xe6\xad\xa4\xe6\x97\xb6'
  • 天涯共此时

五个文字,UTF-8的编码方式比GBK编码方式多了5个字节,编码输出后前面的b代表二进制的意思,用哪种方式编码,就只能用哪种方式解码,不相通,用encode()编码,用decode()解码,括号内的encoding=是用来指定编码或者解码方式的,无论是GBK还是UTF-8都要用字符串的形式填写才能运行

最后修改:2022 年 01 月 08 日 02 : 30 AM