一、序列类型
序列类型支持成员关系操作符(in)、大小计算函数(len())、分片([]),并且是可以迭代的。
python提供了5种内置的序列类型:bytearray、bytes、list、str与tuple,当然python标准库中还提供了其它一些序列类型,最值得注意的是collections.namedtuple。
1、元组
元组是个有序的序列,其中包含0个或多个对象引用。元组支持与字符串一样的分片与步距的语法,这使得从元组提取数据项比较容易。与字符串类似,元组也是固定的,因此,不能替换或者删除其中包含的任意数据项。如果需要修改有序序列,应该使用列表而非元组。如果需要对元组进行修改,需要先使用list()转换函数将其转换为列表,然后在产生的列表之上进行相应修改。
tuple数据类型可以作为一个函数进行调用,tuple()——不指定参数时将返回一个空元组,使用tuple作为参数时将返回该参数的浅拷贝,对其它任意参数,将尝试把给定的对象转换为tuple类型。有时候,元组必须包含在圆括号中,以避免语义的二义性,例如:如果将元组1,2,3传递给一个函数,就应该写成function((1, 2, 3))的形式。
元组只提供了两种方法:t.count(x),返回对象x在元组中出现的次数;t.index(x),返回对象在元组t中出现的最左边位置——在元组中不包含x时,则产生ValueError异常。这些方法对列表也是可用的。
此外,元组可以使用操作符+(连接)、*(赋值)与[](分片),也可以使用in与not in来测试成员关系。并且,虽然元组是固定对象,但+=与*=这两个增强的赋值运算符也可以使用——实际上是Python创建了新元组,用于存放结果,并将左边的对象引用设置为指向新元组。这些操作符应用于字符串时,采用的技术是相同的。元组可以使用标准的比较操作符(<、<=、==、!=、>=、>)进行比较,这种比较实际是逐项进行的(对嵌套项,比如元组内的元组,递归进行处理)。
下面给出几个分片实例,最初是提取一个选项,之后提取项分片:
上面这些处理过程对字符串、列表以及任意其它序列类型都是一样的:
这里本来是想创建一个新的5元组,但结果是一个三元组,其中包含两个二元组,之所以会这样,是因为在3个项(一个元组,一个字符串,一个元组)之间使用了逗号操作符。要得到一个单独的元组,并包含所需项,必须对其进行连接:
要构成一个一元组,逗号是必需的,但在这种情况下,如果仅仅是将逗号放置在其中,就会产生一个TypeError(因为Python会认为我们试图将字符串与元组进行连接),因此,在这里必须同时使用逗号与圆括号。
分片操作符[]可以应用于一个分片,必要的时候可以使用多个,比如:
我们逐步来查看上面的代码,开始处的things[2]表示的是元组的第三项(因为第一项索引值为0),该项本身是一个元组,即("pea", (5, "Xyz"), "queue")。表达式things[2][1]表示things[2]元组的第二项,该项也是一个元组,即(5, "Xyz");表达式things[2][1][1]表示things[2][1]的第二项,即"Xyz";最后,表达式things[2][1][1][2]表示的是字符串中的第三项(字符),即“z”。
元组可以存放任意数据类型的任意项,包括组合类型,比如元组与列表,实际上存放的是对象引用。
2、命名的元组
命名的元组与普通元组一样,有相同的表现特征,其添加的功能就是可以根据名称引用元组中的项,就像根据索引位置一样,这一功能使我们可以创建数据项的聚集。
collections模块提供了namedtuple()函数,该函数用于创建自定义的元组数据类型,例如:
Sale = collections.namedtuple("Sale", "productid custiomerid date quantity price")
collections.namedtuple()的第一个参数是想要创建的自定义元组数据类型的名称,第二个参数是一个字符串,其中包含使用空格分隔的名称,每个名称代表该元组数据类型的一项。第一个参数以及第二个参数中空格分隔开的名称必须都是有效的python字符串。该函数返回一个自定义的类(数据类型),可用于创建命名的元组。因此,这一情况下,我们将Sale与任何其它Python类(比如元组)一样看待,并创建类型为Sale的对象,例如:
这里,我们创建了包含两个Sale项的列表,也就是包含两个自定义元组。我们可以使用索引位置来引用元组中的项——比如,第一个销售相的价格为sales[0][-1](即7.99)——但我们也可以使用名称进行引用,并且这样会更加清晰:
命名的元组提供的清晰与便利通常都是有用的,例如:
3、列表
列表是包含0个或多个对象引用的有序序列,支持与字符串以及元组一样的分片与步距语法,这使得从列表中提取数据项很容易实现。与字符串以及元组不同的是,列表是可变的,因此,我们可以对列表中的项进行删除或替换,插入、替换或删除列表中的分片也是可能的。
list数据类型可以作为函数进行调用,list()——不带参数进行调用时将返回一个空列表;带一个list参数时,返回该参数的浅拷贝;对任意其它参数,则尝试将给定的对象转换为列表。另外,也可以使用列表内涵来创建列表。
由于列表中所有数据项实际上都是对象引用,因此,与元组一样,列表也可以存放任意数据类型的数据项,包括组合数据类型,比如列表与元组。列表可以使用标准的比较操作符(<、<=、==、!=、>=、>)进行比较,这种比较实际是逐项进行的(对嵌套项,比如列表内的元组或列表,递归进行处理)。
和元组类似,列表也支持嵌套、迭代、分片等操作。列表支持使用in与not in进行成员关系测试,使用+进行连接,使用+=进行扩展(即将右边操作数代表的所有项附加到列表中),使用*与*=进行复制等操作。列表也可以用于内置的len()函数以及del语句,del语句在“使用del语句删除项”工具条中进行描述。列表至此append、count、extend、index、insert、pop、remove、reverse、sort等方法。
尽管可以使用分片操作符存取列表中的数据项,但在有些情况下,我们需要一次提取两个或更多个数据项,可以使用序列拆分实现。任意迭代的(列表、元组等)数据类型都可以使用序列拆分操作符进行拆分,即*。用于赋值操作符左边的两个或多个变量时,其中的一个使用*进行引导,数据项将赋值给该变量,而所有剩下的数据项将赋值给带星号的变量,例如:
以这种方式使用序列拆分操作符时,表达式*rest以及类似的表达式称为带星号的表达式。
Python还有一个相关的概念:带星号的参数,例如:
我们可以使用3个参数来调用该函数,也可以使用带星号的参数:
在第一个调用中,我们提供了通常的3个参数。在第二个调用中,我们使用了一个带星号的参数——这里,列表的3个数据项被*拆分,因此,调用函数时,函数将获得这3个参数,使用三元组也可以完成同样的任务。在第三个调用中,第一个参数采用常规的方式进行传递,两外两个参数则是通过对列表L中一个包含两个数据项的数据分片拆分而来。
使用del语句删除项,并不一定是删除数据,而是取消该对象引用到数据项的绑定,并删除对象引用,对象引用被删除后,如果该对象引用所引用的数据项没有被其它对象引用进行引用,那么该数据项将进入垃圾收集流程。
当操作符*出现在赋值操作的左边时,用作拆分操作符,出现在其它位置(比如在函数调用内)时,若用作单值操作符,则代表拆分操作符;若用作二进制操作符,则代表多复制操作符。
4、列表内涵
小列表通常可以使用列表字面值直接创建,但长一些的列表,通常则需要使用程序进行创建。列表内涵是一个表达式,也是一个循环,该循环有一个可选的、包含在方括号中的条件,作用是为列表生成数据项,并且可以使用条件过滤掉不需要的数据项。列表内涵最简单的形式如下:
[item for item in iterable]
上面的语句将返回一个列表,其中包含iterable中的每个数据项,在语义上与list(iterable)是一致的。有两个特点使得列表内涵具有更强大的功能,也更能引起使用者的兴趣,一个是可以使用表达式,另一个是可以附加条件——由此带来如下两种实现列表内涵的常见语法格式:
[expression for item in iterable]
[expression for item in iterable if condition]
第二种语法格式实际上等价于:
通常,上面的语法中,expression或者数据项本身,或者与数据项相关。当然,列表内涵不需要for .. in循环中使用的temp变量。例如:生成给定时间范围内的闰年列表:
由于列表内涵会生成列表,也就是iterable,同时用于列表内涵的语法需要使用iterable,因此,对列表内涵进行嵌套是可能的。这与嵌套的for ... in循环是等价的。如果生成的列表非常大,那么根据需要生成每个数据项会比一次生成整个列表更高效,这可以通过使用生成器实现,而不使用列表内涵。
二、集合类型
Python提供两种内置的集合类型:可变的set类型和固定的frozenset类型。只有可哈希运算的对象可以添加到集合中,可哈希运算的对象包含一个__hash__()特殊方法,其返回值在某个对象的整个生命周期内都是相同的,并可以使用__eq__()特殊方法进行相等性比较(特殊方法——方法名的起始与结尾都使用两个下划线)。所有内置的固定数据类型(比如:float、frozenset、int、str、tuple)都是可哈希运算的,都可以添加到集合中。内置的可变数据类型(比如:dict、list、set)都不是可哈希运算的,因为其哈希值会随着包含项数的变化而变化,因此,这些数据类型不能添加到集合中。
1、集合
集合是0个或多个对象引用的无序组合,这些对象引用所引用的对象都是可哈希运算的。集合是可变的,因此可以很容易地添加或移除数据项,但由于其中的项是无序的,因此,没有索引位置的概念,也不能分片或按步距分片。
set数据类型可以作为函数进行调用,set()——不带参数进行调用时将返回一个空集合;带一个set参数时返回该参数的浅拷贝;对任意其它参数,则尝试将给定的对象转换为集合。非空集合可以不使用set()函数创建,但空集合必须使用set()创建,而不能使用空的圆括号来创建。也可以使用集合内涵来创建集合。
集合中包含的每个数据项都是独一无二的——添加重复的数据项固然不会引发问题,但是毫无意义。集合常用语删除重复的数据项,比如:x是一个字符串列表,在执行x=list(set(x))之后,x中的每个字符串都将是独一无二的——其存放顺序也是任意的。
集合支持内置的len()函数,也支持使用in与not in进行的快速成员关系测试,集合还支持add、clear、copy、difference、discard、intersection、isdisjoint、issubset、issuperset、pop、remove、symmetric_difference、union、update等方法与操作符。
集合数据类型的一个常用场景是进行快速的成员关系测试,另一个常用的场景是确保没有处理重复的数据。
2、集合内涵
集合内涵是一个表达式,也是一个带有可选条件(包含在花括号中)的循环,与列表内涵类似,也支持两种语法格式:
可以使用上面得语法进行过滤(假定顺序不重要),下面给出一个实例:
给定files中的一个文件名列表,上面这一集合内涵使得集合html只存放那些以.htm或.html结尾的文件名,这里不区分大小写。
就像列表内涵一样,集合内涵中使用的iterable本身也可以是一个集合内涵(或任何其它类型的内涵),因此,可以创建相当复杂的集合内涵。
3、固定集合
固定集合是指那种一旦创建后就不能改变的集合。固定集合只能使用frozenset数据类型函数(作为函数进行调用)进行创建,不带参数调用时,frozenset()将返回一个空的固定集合,带一个frozenset参数时,将返回该参数的浅拷贝,对任何其它类型的参数,都尝试将给定的对象转换为一个frozenset。
如果将二元运算符应用于集合与固定集合,那么产生结果的数据类型与左边操作数的数据类型一致。因此,如果f是一个固定集合,s是一个集合,那么f & s将产生一个固定集合,s & f则产生一个集合。
三、映射类型
映射类型是一种支持成员关系操作符(in)与尺寸函数(len())的数据类型,并且也是可以迭代的。映射是键-值数据项的组合,并提供了存取数据项及其键、值的方法。进行迭代时,映射类型以任意顺序提供其数据项。只有可哈希运算的对象可用作字典的键,因此,固定的数据类型(比如:float、frozenset、int、str以及tuple)都可以用作字典的键,可变的数据类型(比如:dict、list与set)则不能。每个键相关联的值实际上是对象引用,可以引用任意类型的对象,包括数字、字符串、列表、集合、字典、函数等。
1、字典
dict是一种无序的组合数据类型,其中包含0个或多个键-值对。其中,键是指向可哈希运算的对象的对象引用,值是可以指向任意类型对象的对象引用。字典是可变的,因此可以很容易地对其进行数据项的添加或移除操作。由于字典是无序的,因此,索引位置对其而言是无意义的,从而也不能进行分片或按步距分片。
dict数据类型可以作为函数调用:dict(),不带参数调用该函数时,将返回一个空字典;带一个映射类型参数时,将返回以该参数为基础的字典。比如:该参数本身为字典,则返回该参数的浅拷贝;如果序列中的每个数据项本身是一个包含两个对象的序列,则第一个用作键,第二个用作值。
字典也可以用花括号创建——空的花括号{}会创建空字典,非空的花括号必须包含一个或多个逗号分隔的项,其中每一项都包含一个键、一个字面意义的冒号以及一个值。另一种创建字典的方式是使用字典内涵。下面是用各种语法创建字典的一些实例:
字典d1是使用字典字面值创建的,字典d2是使用关键字参数创建的,字典d3与d4是从序列中创建的,字典d5是从字典字面值创建的。用于创建字典d4的内置zip()函数返回一个元组列表,其中第一个元组包含的是zip()函数的iterable参数的每一项,第二个元组包含的是每一项的具体值,依次类推。如果键是有效的标识符,那么关键字参数语法(用于创建字典d2)通常是最紧凑与最方便的。
字典的键是独一无二的,因此,如果向字典中添加一个键-值项,并且该键与字典中现存的某个键相同,那么实际的效果是使用新值替换该键的现值。方括号用于存取单独的值,d2[id]返回1948,d2[name]返回“washer”,d2[hello]会导致KeyError异常。
方括号也可以用于添加或删除字典项,要添加一个项,可以使用操作符=,比如:d2["X"] = 59;要删除一个项,可以使用del语句,比如:del d2[size]将从字典中删除键为size的项,如果该键不存在,则会产生一个KeyError异常;使用dict.pop()方法,也可以从字典中移除(并返回)数据项。
字典支持内置的len()函数,也可以使用in与not in对其键进行快速的成员关系测试,字典支持clear、copy、fromkeys、get、items、keys、pop、popitem、setdefault、update、values等方法。字典支持&(intersection)、|(union)、-(difference)、^(symmetric difference)。
2、字典内涵
字典内涵是一个表达式,同时也是一个循环,该循环带有一个可选的条件(包含在方括号中),与集合内涵非常类似。与列表内涵和集合内涵一样,也支持两种语法格式:
字典内涵可用于创建反转的目录,比如:给定字典d,可以生成一个新字典,新字典的键是d的值,值则是d的键:
如果原字典中的所有值都是独一无二的,那么上面生成的新字典又可以发转回原字典——但是如果有任何值不是可哈希运算的,那么这种反转会失败,并产生TypeError异常。
3、默认字典
默认字典与普通字典相比的不同之处在于可以对遗失的键进行处理,而在所有其它方面与普通字典一样。比如:如果有某个普通字典d,其中不包含键为m的项,那么代码x=d[m]会产生KeyError异常,但是如果d创建为默认字典,那么x=d[m]会创建一个新项(键为m,值为默认值),并返回新创建项的值。
四、组合数据类型的迭代与复制
1、迭代子、迭代操作与函数
iterable数据类型每次返回其中的一个数据项。任意包含__iter__()方法的对象或任意序列(也即包含__getitem__()方法的对象,该方法接受从0开始的整数参数)都是一个iterable,并可以提供一个迭代子。迭代子是一个对象,该对象可以提供__next__()方法,该方法依次返回每个相继的数据项,并在没有数据项时产生StopIteration异常。
数据项返回的顺序依赖于底层的iterable。对列表与元组等情况,数据项的返回通常从第一个数据项(索引位置0)开始依序返回,但是有些迭代子也可能以任意顺序返回数据项——比如:用于字典与集合的迭代子。
使用for item in interable循环时,Python在效果上是调用iter(iterable)来获取一个迭代子。之后在每次循环迭代时,将调用该迭代子的__next__()方法以获取下一个数据项,在产生StopIteration异常时,将捕获这一异常,循环终止。获取迭代子的下一项的方法是调用内置的next()函数。
需要注意的是:与任何其它对象一样,python函数本身也是对象,也可以作为参数传递给其它函数,也可以在组合类型中存放,而不需要进行格式化。函数名只是对该函数的一个对象引用,只有在函数名后跟随圆括号时,Python才会将其视为对函数进行调用。
在传递一个Key函数时,对列表中的每一项(项将作为函数的唯一参数),将调用一次该函数,并创建“修饰后”的列表,之后对修饰后列表排序,并将排序后的列表(不带修饰)作为结果返回。我们可以自由地使用自定义函数作为key函数。比如:通过将str.lower()方法作为key进行传递,我们可以对字符串列表进行大小写不敏感的排序,假定列表x为["Sloop", "Yawl", "Cutter", "schooner", "ketch"],x = sorted(x, key=str.lower),将生成一个新列表:["Cutter", "ketch", "schooner", "Sloop", "Yawl"]。Python的排序算法是一种自适应的稳定混合排序算法,在对整数、字符串或其它简单数据类型的组合进行排序时,使用的是其“小于”操作符(<)。
2、组合类型的复制
由于Python使用了对象引用,因此在使用赋值操作符(=)时,并没有进行复制操作。如果右边的操作数是字面值,比如字符串或数字,那么左边的操作数被设置为一个对象引用,该对象引用将指向存放字面值的内存对象。如果右边的操作数是一个对象引用,那么左边的操作数将设置为一个对象引用,并与右边的操作数指向相同的对象。这种机制的好处之一是可以非常高效地进行赋值操作。
在对很大的组合类型变量进行赋值时,比如长列表,这种机制带来的高效是非常明显的:
这里,创建了一个新的对象引用beatles,两个对象引用指向的是同一个列表——没有进行列表数据本身的复制。
由于列表是可变的,因此可以对其进行改变,比如:
我们使用变量bealtes进行了改变——但beatles是一个对象引用,并与songs指向同一列表。因此,通过哪一个对象引用进行的改变对另一个对象引用都是可见的。
然而,有时又确实需要组合类型数据(或其它可变对象)的一个单独的副本。对序列,在提取数据片时——比如songs[:2],数据片总是取自某个数据项的一个单独的副本,因此,如果需要整个序列的副本,而不仅仅是一个对象引用,则可以通过下面的方式实现:
对字典与集合而言,这种复制操作可以使用dict.copy()与set.copy()来实现,也可以使用copy模块提供的copy.copy()函数。需要注意的是:这些复制技术都是浅拷贝——也就是说,复制的只是对象引用,而非对象本身。对固定数据类型,比如数字与字符串,这与复制的效果是相同的,但对于可变的数据类型,比如嵌套的组合类型,这意味着相关对象同时被原来的组合与复制得来的组合引用。请看下面实例:
在对列表进行浅拷贝时,对嵌套列表["A", "B", "C"]的引用将被复制。这意味着,x与y都将其第三项作为指向这一列表的对象引用,因此,对嵌套列表的任何改变,对x与y都是可见的。如果我们确实需要对一个独立的副本或任意的嵌套组合,可以进行深拷贝:
这里,列表x与y,及其所包含的列表项,都是完全独立的。