Python 3数据类型之序列

Python 3数据类型之序列

Python 3中的序列类型包括字符串str、字节串bytes、元组tuple以及列表list等。本文通过一些简单的代码来演示序列类型的用法并揭示其实现原理。

字符串str

最简单的使用字符串的方式就是字面值字符串,即用引号括起来的字符串。Python中表示字符串可以用单引号’’,也可以用双引号"",可根据个人习惯而定。对于单行字符串,如果比较长,在源码中一行写不下需要多行,则可以使用续行符\,应注意续行符后面不应该有任何字符。反之,如果要使用多行字符串,一种方法是以转义字符\n来表示换行,另一种方法则是使用三引号来表示。三引号字符串的好处是很直观,引号里写成什么样子显示就是什么样子,跟shell里的here document有些类似。

type('hello')
str
type("hello")
str
s='Hello \
World'
print(s)
Hello World
s='Hello\nWorld'
print(s)
Hello
World
s="""  Hello, World!
Greeting from Python 
with three lines."""
print(type(s))
print(s)
<class 'str'>
  Hello, World!
Greeting from Python 
with three lines.

字符串的一个作用是用做文档帮助字符串。模块、函数和类型都可以有帮助字符串。模块的帮助字符写在文件开头,函数的文档字符串写在函数定义的开头,类型的文档字符串则写在类型定义的开头。一般为了可读性更好,都采用三引号字符串作为文档字符串。模块,函数和类型的文档字符串会被解释器收集到模块对应的模块对象、函数对象和类型对象的__doc__属性中。当然也会在help命令显示相应的模块、函数和类型帮助信息时打印出来。

"""
A test module with help text.
In this module, there is test_func, TestClass and TestClass2.
"""
def test_func():
    """A test function with help text."""
    print('test_func')

class TestClass:
    'A test class with help text.'
    pass

class TestClass2:
    """Another test class with three quotes help text."""
    pass

print(__doc__)
print(test_func.__doc__)
print(TestClass.__doc__)
print(TestClass2.__doc__)
A test module with help text.
In this module, there is test_func, TestClass and TestClass2.

A test function with help text.
A test class with help text.
Another test class with three quotes help text.

字符串属于序列类型,因此它支持下标索引。Python的索引支持正向索引,下标从0到len-1分别表示第一个到最后一个字符;也支持负数反向索引,-1到-len别表示最后一个字符到第一个字符。字符串之所因能支持下标随机索引,是因为str类型实现了__getitem__接口方法。下面的例子演示了用正向、反向索引和__getitem__方法。

s='string'
print(s[0], s[-6], s.__getitem__(0))
print(s[2], s[-3], s.__getitem__(4))
print(s[5], s[-1], s.__getitem__(5))
s s s
r i n
g g g

还可以使用索引范围方便地截取字符子串。索引范围包括起始索引,但是不包括结束索引。按照数学区间的写法,范围索引的含义为[起始索引,结束索引)。如果不指定起始索引则从第一个字符开始(包括),如果不指定结束索引则到最后一个字符(包括)。因此s[:]就是取整个字符串。见下面几个例子

s="second string"
print(s[:])
print(s[2:8])
print(s[3:])
print(s[:-4])
print(s[1:-1])
second string
cond s
ond string
second st
econd strin

字符串属于序列类型,而序列都继承了容器接口,支持迭代,因此除了可以用索引遍历,还可以用下面演示的迭代器和反向迭代器进行遍历。

s="third string"
s_iter=iter(s)
s_r_iter=reversed(s)
try:
    while True:
        print(next(s_iter), next(s_r_iter))
except StopIteration:
    pass
t g
h n
i i
r r
d t
  s
s  
t d
r r
i i
n h
g t

字符串类支持+和*这两个数学操作符,但是与数字类型的操作含义不同,+表示连接两个字符串,*则表示将字符串重复多次。字符串也支持比较运算符。

s='Hello ' + 'world'
print(s)
s='Great ' * 3
print(s)
s='Hello'
print(s == "Hello")
Hello world
Great Great Great 
True

字符串是不可变(immutable)序列,也就是说一个字符串对象构造好后,这个对象就不会再改变。如果要改变字符串,则会生成一个新的字符串对象。下面的两个例子说明了这一点。第一个例子中调用字符串对象的upper方法后,得到的大写字符串其实是一个新的str对象,而原来的字符串并未变,从对象id也可以印证;第二个例子将在原字符串后增加一个’.’,实际上新的字符串也是一个新的str对象,而原来的str对象其实并未改变,新str对象赋值给了s。在某种程度上,immutable类型可理解为类型实例为read-only,new instance on write。

s='immutable string'
s1=s
print('id: ', id(s), id(s1))

s.upper()
print(s)
print('id: ', id(s))

s += '.'
print(s)
print('id: ', id(s), id(s1))
id:  4354609760 4354609760
immutable string
id:  4354609760
immutable string.
id:  4354562688 4354609760

字符串str类实现了很多字符串的操作方法,如join、split、find等,详细信息可在Python中用help(str)命令查看。下面是一些简单例子。

s='.'.join(['www', 'google', 'com'])
print(s)
print(s.split('.'))

print(s.index('google'))
print(s.find('google'))
print(s.count('google'))
print('key=value'.partition('='))
www.google.com
['www', 'google', 'com']
4
4
1
('key', '=', 'value')

str类有一个format方法,生成格式化后的字符串。字符串中用{}表示占位符,而format方法传入的参数则替换这些占位符。在Python 3中,这可以简写为f字符串,即以前缀f开头的字符串。见下面的例子。

year=2021
mon='Jan'
day=22
date_str = '{} {}, {}'.format(mon, day, year)
print(date_str)
date_str = f'{mon} {day}, {year}'
print(date_str)
Jan 22, 2021
Jan 22, 2021

字节串

字节串的类型是内置的bytes类型。字节串的字面值对象以b开头,后面带引号串,形式上跟f字符串很像。一般情况下,字符串引号内都是可打印字符,而字节串则可以有不可打印字符。虽然在字符串内也可以用转义字符来表示不可打印字符,但是在显示时的处理是不一样的,见下面的例子。因此,如果串中又不可打印字符,一般用字节串更方便。

bs=b'\x03\x00\x61\x62\x63'
print(type(bs), bs, len(bs))
s='\x03\x00\x61\x62\x63'
print(type(s), s, len(s))
<class 'bytes'> b'\x03\x00abc' 5
<class 'str'> abc 5

除了采用字面值对象(literal object),字节串对象还可以用其它方式构造,如下所示。下面的例子中bytes对象可以通过str对象来构造,也可以通过bytes,还可以通过序列类型的迭代器来构造。这要归功于如下所示的bytes类的构造函数。这一点与str对象有所不同。

class bytes(object)
 |  bytes(iterable_of_ints) -> bytes
 |  bytes(string, encoding[, errors]) -> bytes
 |  bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
    
class str(object)
 |  str(object='') -> str
 |  str(bytes_or_buffer[, encoding[, errors]]) -> str
bs=bytes(range(11))
print(bs)
bs=bytes(b'\x01\x02\x03')
print(bs)
bs=bytes(reversed(bs))
print(bs)
bs=bytes([4, 5, 6])
print(bs)
bs=bytes('hello', 'utf-8')
print(bs)
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n'
b'\x01\x02\x03'
b'\x03\x02\x01'
b'\x04\x05\x06'
b'hello'

跟字符串str类似,字节串也是不可变序列。因此也支持正反索引、正反迭代等操作,并且字节串类bytes支持的方法跟字符串也很类似。

bs=bytes(range(256))
print(bs[0x61:0x7b])
print(bs[-16:-1])
print(bytes(reversed(bs[0x61:0x7b])))
print(b' '.join([b'\x01', b'\x02', b'\x03']))
print(b'\x01\x02\x03' + b'\0x04\x05\x06')
print(b'\x01\x02\x03' * 3)
b'abcdefghijklmnopqrstuvwxyz'
b'\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe'
b'zyxwvutsrqponmlkjihgfedcba'
b'\x01 \x02 \x03'
b'\x01\x02\x03\x00x04\x05\x06'
b'\x01\x02\x03\x01\x02\x03\x01\x02\x03'

元组tuple

元组也是一种不可变序列类型(immutable sequence),有Python的内置类型tuple实现。元组采用()来表示,元素之间用逗号’,'隔开。用字面值来构造元组对象时,也可以用参数列表的形式,即不带(),如下面所示。

t=(1,2,3)
print(type(t), t, len(t))
t=()
print(type(t), t, len(t))
print(type((4,5,6)))
t=7,8,9
print(t)
<class 'tuple'> (1, 2, 3) 3
<class 'tuple'> () 0
<class 'tuple'>
(7, 8, 9)

前面提到的序列类型str,其中每个元素都是一个字符;而序列类型bytes,每个元素都是字节。元组则不一样,其每个元素都可以是任意对象,甚至可以是元组本身。如此则可以多层次嵌套下去。参见下面的例子。

t=(1, 'hello', b'\x01\x02\x03', object(), [1, 2, 3], (4, 5, (6, 7)), ())
print(type(t), t)
print(len(t))
for i in range(len(t)):
    print(f'tuple element #{i}: {type(t[i])} {t[i]}')
<class 'tuple'> (1, 'hello', b'\x01\x02\x03', <object object at 0x1073ce0c0>, [1, 2, 3], (4, 5, (6, 7)), ())
7
tuple element #0: <class 'int'> 1
tuple element #1: <class 'str'> hello
tuple element #2: <class 'bytes'> b'\x01\x02\x03'
tuple element #3: <class 'object'> <object object at 0x1073ce0c0>
tuple element #4: <class 'list'> [1, 2, 3]
tuple element #5: <class 'tuple'> (4, 5, (6, 7))
tuple element #6: <class 'tuple'> ()

类似于bytes的构造函数,tuple的构造函数也支持传入可迭代(iterable)对象进行构造。不过bytes构造函数只支持可迭代的整数,而tuple构造函数则是任意可迭代对象。所谓可迭代对象,就是实现了__iter__方法,可返回迭代器的对象,如str, bytes, tuple及其对应的正向、反向迭代器都是。下面的例子演示了tuple对象的构造。

class tuple(object)
 |  tuple(iterable=(), /)
# from tuple
t=tuple((1,2,3))
print(t)
# from str
t=tuple('abcd')
print(t)
# from bytes
t=tuple(b'\x0a\x0b\x0c')
print(t)
# from iterator
t=tuple(reversed('Python tuple'[-5:]))
print(t)
# from iterator
t=tuple(reversed(t))
print(t)
# from filter (iterable)
t=tuple(filter(lambda x: x%2==1, range(20)))
print(t)
# from list
t=tuple([1,2,3])
print(t)
(1, 2, 3)
('a', 'b', 'c', 'd')
(10, 11, 12)
('e', 'l', 'p', 'u', 't')
('t', 'u', 'p', 'l', 'e')
(1, 3, 5, 7, 9, 11, 13, 15, 17, 19)
(1, 2, 3)

从前面的例子可以看出,在构造序列对象时,可以采用赋值语句的方式,赋值语句左值为对象,右值则为字面值,这个过程好像把右边的一序列字面值打包到一个序列类型的对象中。事实上赋值语句也可以反过来,左值为一序列对象,右值为Pythone序列类型的对象,此时Python会将序列类型的对象自动解开成元素序列,然后按序赋值给左边对象。
在这种用法中,如果左边定义的变量前面带*号,则表示该变量为列表list类型, 而且从右边的序列中构造,哪怕只有一个元素或者没有元素。如下面的例子所示。

# unpack str
a,b,c='abc'
print(type(a),a,b,c)
# unpack bytes
a,b,c=b'\x01\x02\x03'
print(type(a),a,b,c)
# unpack list
a,b,c=[1,2,3]
print(type(a),a,b,c)
# unpack tuple
tp_args=(100, 'hello', b'\x01\x02\x03', (), (1, 2,3), [1,2,3])
a,b,c,d,e,f=tp_args
print(type(a), a)
print(type(b), b)
print(type(c), c)
print(type(d), d)
print(type(e), e)
print(type(f), f)

first, *middle, last = 78, 85, 86, 87, 99
print(type(first), first)
print(type(middle), middle)
print(type(last), last)

first, *middle, last = 65, 100
print(type(middle), middle)
<class 'str'> a b c
<class 'int'> 1 2 3
<class 'int'> 1 2 3
<class 'int'> 100
<class 'str'> hello
<class 'bytes'> b'\x01\x02\x03'
<class 'tuple'> ()
<class 'tuple'> (1, 2, 3)
<class 'list'> [1, 2, 3]
<class 'int'> 78
<class 'list'> [85, 86, 87]
<class 'int'> 99
<class 'list'> []
a,*b,c=1,2,3,4
print(b)
[2, 3]

tuple的一个重要应用是用做函数参数。将前面赋值的情况延伸一下,就可以得到函数调用时的参数传递情况。被调用函数有参数定义,而调用函数则提供参数,这个过程可以理解为将调用函数提供的参数行赋值给被调用函数定义的参数,以生成对应的参数对象。在定义函数参数时,如果在参数面前带*,则表示该参数是一个tuple类型的对象。这与前面多变量赋值时,如果定义的变量前面带*则表示该变量是list类型的情况很类似。只不过函数参数是tuple只读的,而变量则是list可写的。当然,如果参数前面没有*则是普通参数。请参考下面的例子总结普通参数与元组参数的用法与区别。

  • 在调用带tuple类型的参数的函数时,tuple类型的参数的构造是根据调用者传递的每个对象进行的,每个对象都会作为tuple类型的参数的一个元素,哪怕传入的是一个序列类型的对象,它也仅仅是作为tuple类型的参数的一个元素。比如在例子test_func_tp(‘abc’),传入了一个序列str类型的对象,因此构造的函数参数pos_args为(‘abc’),只有1个元素的元组。如果想要把序列解开,作为多个元素使用,则应在调用者处使用*。这种用法仅限于调用函数时解开序列。
  • 例子test_func_tp2(1, 2, 3,(4,5))能够更直观地说明tuple参数的构造,可以理解为函数参数是按照以下类似于赋值的方式构造的,只不过是tuple类型而不是list类型:
arg1,*pos_args=1, 2, 3,(4,5)

因此,arg1为1,而pos_args则为(2,3,(4,5))。

  • 调用print函数时,也可以用*解开序列,用法类似。
def test_func(arg):
    print('test_func arg', type(arg), arg)
def test_func_tp(*pos_args):
    print('test_func_tp arg', type(pos_args), pos_args)
def test_func_tp2(arg1, *pos_args):
    print('test_func_tp2 arg', type(pos_args), pos_args)
    
test_func_tp(1)
#not work, only one arg
#test_func_tp(1,2,3)
test_func_tp(1,2,3)
test_func('abc')
test_func_tp('abc')
#not work, only one arg
#test_func(*'abc')
test_func_tp(*'abc')
tp_args=(1,2,3)
test_func(tp_args)
test_func_tp(tp_args)
#not work, only one arg
#test_func(*tp_args)
test_func_tp(*tp_args)
test_func_tp2(1, 2, 3,(4,5))

print(*'hello')
print(*tp_args)
test_func_tp arg <class 'tuple'> (1,)
test_func_tp arg <class 'tuple'> (1, 2, 3)
test_func arg <class 'str'> abc
test_func_tp arg <class 'tuple'> ('abc',)
test_func_tp arg <class 'tuple'> ('a', 'b', 'c')
test_func arg <class 'tuple'> (1, 2, 3)
test_func_tp arg <class 'tuple'> ((1, 2, 3),)
test_func_tp arg <class 'tuple'> (1, 2, 3)
test_func_tp2 arg <class 'tuple'> (2, 3, (4, 5))
h e l l o
1 2 3

列表list

列表类型采用[]来表示。它和元组很类似,每个元素都可以是任意类型的对象。它也可以通过任意的可迭代对象来构造。

class list(object)
 |  list(iterable=(), /)
print(type([1,2,3]))
l=[1, 'hello', b'\x01\x02\x03', object(), [1, 2, 3], (4, 5, (6, 7)), ()]
print(type(l), l)
print(len(l))
for i in range(len(l)):
    print(f'list element #{i}: {type(l[i])} {l[i]}')
    
# from filter (iterable)
l=list(filter(lambda x: x%2==1, range(20)))
print(l)
<class 'list'>
<class 'list'> [1, 'hello', b'\x01\x02\x03', <object object at 0x1073cee70>, [1, 2, 3], (4, 5, (6, 7)), ()]
7
list element #0: <class 'int'> 1
list element #1: <class 'str'> hello
list element #2: <class 'bytes'> b'\x01\x02\x03'
list element #3: <class 'object'> <object object at 0x1073cee70>
list element #4: <class 'list'> [1, 2, 3]
list element #5: <class 'tuple'> (4, 5, (6, 7))
list element #6: <class 'tuple'> ()
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

list和tuple最大的区别在于,list是可变序列(mutable sequence),即可改写的,而tuple则是不可变序列。一个list类型的对象,由于它是可变的,因此如果给它增加或者删除元素,这个对象还是原来的对象;而一个tuple类型的对象,由于它是不可变的,因此要增加或者删除元素,只能新建一个对象再进行操作。下面的例子通过对象的id来直观地说明了这一点。

l=[1,2,3]
print(id(l), l)
l*=2
print(id(l), l)
t=(1,2,3)
print(id(t),t)
t*=2
print(id(t),t)
4428886336 [1, 2, 3]
4428886336 [1, 2, 3, 1, 2, 3]
4417594112 (1, 2, 3)
4417085984 (1, 2, 3, 1, 2, 3)

在使用中必须注意这一特性,否则就容易造成问题。尤其是在调用函数时,如果传递的是可变序列对象的引用,那么对象可能会在函数里面被修改

def test_func(arg):
    arg += type(arg)(arg)
    print('inside test_func:', id(arg), arg)
    
test_arg=[1,2,3]
test_func(test_arg)
print('after test_func:', id(test_arg), test_arg)

test_arg=(1,2,3)
test_func(test_arg)
print('after test_func:', id(test_arg), test_arg)
inside test_func: 4428887168 [1, 2, 3, 1, 2, 3]
after test_func: 4428887168 [1, 2, 3, 1, 2, 3]
inside test_func: 4418494080 (1, 2, 3, 1, 2, 3)
after test_func: 4417198272 (1, 2, 3)

list类型提供了一个copy方法来复制一个列表对象,起作用与[:]类似。这种复制会新建一个对象,再把原对象的元素复制过去。而在复制每个元素时,如果这个元素是一个引用,那么复制过去也是一个引用。也就是说,对于每个元素而言,并没有做类似copy方法的事情,而只是做了简单的赋值。因此,如果list对象的某个元素也是list,那么copy方法并不会复制这个元素list里的每个元素。所以,copy方法被称为浅拷贝(shallow copy)。与此相反的概念则是深拷贝(deep copy)。从下面的例子可以看出:

  • l_copy实际上和l是一个对象,它们是引用关系,因此是完全的浅拷贝
  • l_copy2和l_copy3虽然和l不是一个对象,但是列表的第三个元素其实还是一个引用,都是同一个list对象l_sub,因此这个元素是浅拷贝,整体也是浅拷贝。
l_sub=['a', 'b']
print(id(l_sub))
l=[1,2,l_sub]
l_copy=l
l_copy2=l[:]
l_copy3=l.copy()

print(id(l), id(l[2]), l)
print(id(l_copy), id(l_copy[2]), l_copy)
print(id(l_copy2), id(l_copy2[2]), l_copy2)
print(id(l_copy3), id(l_copy3[2]), l_copy3)
print('after append:')
l.append(3)
l[2].append('c')
print(id(l), id(l[2]), l)
print(id(l_copy), id(l_copy[2]), l_copy)
print(id(l_copy2), id(l_copy2[2]), l_copy2)
print(id(l_copy3), id(l_copy3[2]), l_copy3)
4428868736
4428865792 4428868736 [1, 2, ['a', 'b']]
4428865792 4428868736 [1, 2, ['a', 'b']]
4428866496 4428868736 [1, 2, ['a', 'b']]
4428868544 4428868736 [1, 2, ['a', 'b']]
after append:
4428865792 4428868736 [1, 2, ['a', 'b', 'c'], 3]
4428865792 4428868736 [1, 2, ['a', 'b', 'c'], 3]
4428866496 4428868736 [1, 2, ['a', 'b', 'c']]
4428868544 4428868736 [1, 2, ['a', 'b', 'c']]

对于不可变序列,由于对象创建好后就不可变,因此用浅拷贝就行,没有深拷贝的需求。这也是不可变序列在使用上比可变序列更简单的原因。

序列类型的接口与实现

前述序列类型的实现都在内置模块builtins中,而这些类型的接口定义则在collections.abc模块中。abc表示abstract base class,即抽象基类,一般用于接口定义。不可变序列都继承自接口类collections.abc.Sequence,而可变序列则继承自接口类collections.abc.MutableSequence。

import collections
print(issubclass(str,collections.abc.Sequence))
print(issubclass(bytes,collections.abc.Sequence))
print(issubclass(bytes,collections.abc.Sequence))
print(issubclass(list,collections.abc.MutableSequence))
True
True
True
True

collections.abc.Sequence和collection.abc.MutableSequence这两个接口类其实是复合接口类,它们又继承了多个接口类,比如Container,Sized,Iterable和Reversible等接口类。这些接口类的层次结构如下所示。

CLASSES
    builtins.object
        Container
        Hashable
        Iterable
            Iterator
                Generator
            Reversible
                Sequence(Reversible, Collection)
                    ByteString
                    MutableSequence
        Sized
            Collection(Sized, Iterable, Container)

下面是对序列类型的主要接口类的方法的总结。

  • Container: 主要方法**__contains__()**, 用于判断序列是是否含有每个元素。实现类中的find, index等方法都依赖于这个方法
  • Iterable: 主要方法**__iter__()**, 用于返回一个迭代器。一个序列如果能返回迭代器,就可以用迭代器遍历(迭代)这个序列,这也是这个接口类名Iterable的含义。不过遍历的迭代则是由迭代器完成的。调用iter()函数时,其实也就是调用对象的__iter__()方法。
  • Reversible: 主要方法**__reversed__()**,用于返回一个逆向迭代器。逆向迭代器用于逆向遍历一个序列。调用reverse()函数时,其实也就是调用对象的__reversed__()方法。
  • Sized: 主要方法**__len__()**,用于返回序列长度(元素个数)。调用len()函数时,其实就是调用对象的__len__()方法。
  • Sequence: 除了继承的方法,还定义了**__getitem__()**方法,用于获取指定位置的元素。这时序列能支持下标索引的基础。
  • Iterator: 迭代器的基类,除了从Iterable继承的__iter__()方法,还定义了**__next__()**方法。迭代器遍历正是通过这两个方法。每种序列类型的实现,都有相应的迭代器类的实现。比如str类,有类str_iterator。
上一篇:python学习记录二


下一篇:全面解读PostgreSQL和Greenplum的Hash Join