1.11 集合
Python是一个发展的语言,尽管前面已经有好几种对象类型了,但是,从实践的角度看还不够,所以,又有了名曰“集合”的这种类型。当然,对象类型本质上是自己可以定义的,要想学会自己定义,请继续阅读本书,不要半途而废,“而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也。”(王安石《游褒禅山记》)。
另外,也不要担心记不住,你只要记住爱因斯坦说的就好了:
爱因斯坦在美国演讲,有人问:“你可记得声音的速度是多少?你如何记下许多东西?”
爱因斯坦轻松答道:“声音的速度是多少,我必须查辞典才能回答。因为我从来不记在辞典上已经印着的东西,我的记忆力是用来记忆书本上没有的东西。”
多么霸气的回答,这回答不仅霸气,还告诉我们一种方法:只要是能够通过某种方法查找到的,就不需要记忆。
各种对象类型都可以通过下述方法但不限于这些方法查到:
- 交互模式下用dir()或者help()。
- 使用Google。上述方法从本书开始就不断强调和示范,目的就在于提示读者要掌握方法,而不是记忆知识。
对学过的对象类型做个归纳整理:
- 能够索引的,如list/str,其中的元素可以重复。
- 可变的,如list/dict,即其中的元素/键值对可以原地修改。
- 不可变的,如str/int,即不能进行原地修改。
- 无索引序列的,如dict,即其中的元素(键值对)没有排列顺序。
1.11.1 创建集合
集合的英文是set,翻译过来叫作“集合”。它的特点是:有的可变,有的不可变;元素无次序,不可重复。
如果说元组(tuple)算是列表(list)和字符串(str)的杂合,那么集合(set)则可以堪称是list和dict的杂合。
集合拥有类似字典的特点:可以用{}花括号来定义;其中的元素没有序列,也就是非序列类型的数据;而且集合中的元素不可重复,这就类似于dict键。
集合也有一点列表的特点:有一种集合可以在原处修改。
通过实验,逐步理解创建set的方法:
- >>> s1 = set("qiwsir")
- >>> s1
- set(['q', 'i', 's', 'r', 'w'])
把字符串中的字符拆解开形成了集合。特别注意观察:qiwsir中有两个i,但是在s1中只有一个i,也就是集合中元素不能重复。
- >>> s2 = set([123, "google", "face", "book", "facebook", "book"])
- >>> s2
- set(['facebook', 123, 'google', 'book', 'face'])
在创建集合的时候,如果发现了重复的元素,就会过滤一下,剩下不重复的。而且,从s2的创建可以看出,查看结果时显示的元素排列顺序与开始建立时不同,完全是随意显示的(怎么能说明是随机的呢?读者有没有办法?),这说明集合中的元素没有序列。
- >>> s3 = {"facebook", 123} #通过{}直接创建
- >>> s3
- set([123, 'facebook'])
除了用set()来创建集合,还可以使用{}的方式,但是这种方式不提倡使用,因为在某些情况下,Python搞不清楚是字典还是集合。看看下面的探讨就发现问题了。
- >>> s3 = {"facebook", [1,2,'a'], {"name":"python", "lang":"english"}, 123}
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: unhashable type: 'dict'
- >>> s3 = {"facebook", [1,2], 123}
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: unhashable type: 'list'
从上述实验可以看出,通过{}无法创建含有列表或者字典类型对象元素的集合。
认真阅读报错信息,有这样的词汇:“unhashable”,在理解这个词之前,先看它的反义词“hashable”,很多时候翻译为“可哈希”,其实它有一个不是音译的名词“散列”。如果我们简单点理解,某数据“不可哈希”(unhashable)就是其可变,如列表和字典都能原地修改,就是unhashable。否则,不可变的,类似字符串那样不能原地修改的就是hashable(可哈希)。
对于字典类型的对象,其“键”必须是hashable,即不可变。
现在遇到的集合,其元素也是“可哈希”的。上面的例子,试图将字典、列表作为元素的元素,就报错了。而且报错信息中明确告知列表和字典是不可哈希类型,言外之意,里面的元素都应该是可哈希类型。
继续探索另外一种情况:
- >>> s1
- set(['q', 'i', 's', 'r', 'w'])
- >>> s1[1] = "I"
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: 'set' object does not support item assignment
这里报错,进一步说明集合不是序列类型,不能用索引方式对其进行修改。
根据前面的经验,类型名称函数能够实现类型转换,比如str()就是将对象转化为字符串,同理,分别用list()和set()能够实现集合和列表两种对象之间的转化。
- >>> s1
- set(['q', 'i', 's', 'r', 'w'])
- >>> lst = list(s1)
- >>> lst
- ['q', 'i', 's', 'r', 'w']
- >>> lst[1] = "I"
- >>> lst
- ['q', 'I', 's', 'r', 'w']
特别说明,利用set()建立起来的集合是可变集合,可变集合都是unhashable类型的。
1.11.2 集合的函数
把与集合有关的函数找出来。
- >>> dir(set)
- ['__and__', '__class__', '__cmp__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']
为了看清楚,我把双画线“__”先删除掉:
- 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update'
然后用help()可以找到每个函数的具体使用方法。
1.add和update
- >>> help(set.add)
- Help on method_descriptor:
- add(...)
- Add an element to a set.
- This has no effect if the element is already present.
在交互模式中,可以看到:
- >>> a_set = {} #我想当然地认为这样也可以建立一个set
- >>> a_set.add("qiwsir") #报错!看错误信息。
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- AttributeError: 'dict' object has no attribute 'add'
- >>> type(a_set) #Python认为我建立的是一个dict
- <type 'dict'>
{}这个东西在dict和set中都有用,但是,若按照上面的方法则建立的是dict,而不是set。这是Python规定的,要建立set,只能用前面已经看到的创建方法了。
- >>> a_set = {'a','i'}
- >>> type(a_set)
- <type 'set'>
- >>> a_set.add("qiwsir") #增加一个元素
- >>> a_set #原地修改
- set(['i', 'a', 'qiwsir'])
- >>> b_set = set("python")
- >>> type(b_set)
- <type 'set'>
- >>> b_set
- set(['h', 'o', 'n', 'p', 't', 'y'])
- >>> b_set.add("qiwsir")
- >>> b_set
- set(['h', 'o', 'n', 'p', 't', 'qiwsir', 'y'])
- >>> b_set.add([1,2,3]) #列表是不可哈希的,集合中的元素应该是hashable类型。
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: unhashable type: 'list'
- >>> b_set.add('[1,2,3]') #可以这样!
- >>> b_set
- set(['[1,2,3]', 'h', 'o', 'n', 'p', 't', 'qiwsir', 'y'])
除了add()之外,还能够从另外一个集合中合并过来元素,方法是set.update(s2)。
- >>> help(set.update)
- update(...)
- Update a set with the union of itself and others.
- >>> s1
- set(['a', 'b'])
- >>> s2
- set(['github', 'qiwsir'])
- >>> s1.update(s2)
- >>> s1
- set(['a', 'qiwsir', 'b', 'github'])
- >>> s2
- set(['github', 'qiwsir'])
2.pop,remove,discard,clear
- >>> help(set.pop)
- pop(...)
- Remove and return an arbitrary set element.
- Raises KeyError if the set is empty.
- >>> b_set
- set(['[1,2,3]', 'h', 'o', 'n', 'p', 't', 'qiwsir', 'y'])
- >>> b_set.pop() #从set中任意选一个删除,并返回该值
- '[1,2,3]'
- >>> b_set.pop()
- 'h'
- >>> b_set.pop()
- 'o'
- >>> b_set
- set(['n', 'p', 't', 'qiwsir', 'y'])
- >>> b_set.pop("n") #如果要指定删除某个元素就报错了
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: pop() takes no arguments (1 given)
set.pop()是从集合中随机选一个元素删除并将这个值返回,但是不能指定删除某个元素。报错信息告诉我们,pop()不能有参数。此外,如果集合是空的了,再做pop()操作也报错。
但是否可以删除指定元素?如果可以,怎么办?
- >>> help(set.remove)
- remove(...)
- Remove an element from a set; it must be a member.
- If the element is not a member, raise a KeyError.
会有办法的,那么多聪明人早就为读者想好了一个函数——remove()——set.remove(obj)中的obj,必须是set中的元素,否则就报错。
- >>> a_set
- set(['i', 'a', 'qiwsir'])
- >>> a_set.remove("i")
- >>> a_set
- set(['a', 'qiwsir'])
- >>> a_set.remove("w")
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- KeyError: 'w'
跟remove(obj)类似的还有discard(obj):
- >>> help(set.discard)
- discard(...)
- Remove an element from a set if it is a member.
- If the element is not a member, do nothing.
与help(set.remove)进行信息对比,看看有什么不同。关键在于如果discard(obj)中的obj是set中的元素就删除,如果不是,就什么也不做。新闻就要对比着看才有意思,这里也一样。
- >>> a_set.discard('a')
- >>> a_set
- set(['qiwsir'])
- >>> a_set.discard('b')
- >>>
在删除上还有一个绝杀,就是set.clear(),它的功能是:Remove all elements from this set.(自己在交互模式下help(set.clear))。
- >>> a_set
- set(['qiwsir'])
- >>> a_set.clear()
- >>> a_set
- set([])
- >>> bool(a_set) #空了,bool一下返回False.
- False
1.11.3 补充知识
集合也是一个数学概念(以下定义来自维基百科):
最简单的说法是最原始的集合论——朴素集合论中的定义,集合就是“一堆东西”。集合里的“东西”叫作元素。若然x是集合A的元素,记作x∈A。
集合是现代数学中一个重要的基本概念。集合论的基本理论直到19世纪末才被创立,现在已经是数学教育中一个普遍存在的部分,在小学时就开始学习了。这里对被数学家们称为“直观的”或“朴素的”集合论进行一个简短而基本的介绍;更详细的分析可见朴素集合论。对集合进行严格的公理推导可见公理化集合论。
在计算机中集合是什么呢?自维基百科是这么说的:
在计算机科学中,集合是一组可变数量的数据项(也可能是0个)的组合,这些数据项可能共享某些特征,需要以某种操作方式一起进行操作。一般来讲,这些数据项的类型是相同的,或基类相同(若使用的语言支持继承)。列表(或数组)通常不被认为是集合,因为其大小固定,但事实上它常常在实现中作为某些形式的集合使用。
集合的种类包括列表、集、多重集、树和图。枚举类型可以是列表或集。
1.11.4 不变的集合
以set()创立的集合都是可原地修改的集合,或者说是可变的,也可以说是unhashable。
还有一种集合不能原地修改,这种集合的创建方法是用frozenset(),顾名思义,这是一个被“冻结”的集合,当然是不能修改的,这种集合就是hashable类型——可哈希。
- >>> f_set = frozenset("qiwsir")
- >>> f_set
- frozenset(['q', 'i', 's', 'r', 'w'])
- >>> f_set.add("python") #报错,不能修改
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- AttributeError: 'frozenset' object has no attribute 'add'
- >>> a_set = set("github") #对比看一看
- >>> a_set
- set(['b', 'g', 'i', 'h', 'u', 't'])
- >>> a_set.add("python")
- >>> a_set
- set(['b', 'g', 'i', 'h', 'python', 'u', 't'])
1.11.5 集合运算
唤醒中学数学(准确说是高中数学中的一点知识)中关于集合的知识,当然,如果你是某个理工科的专业大学毕业,应该更熟悉集合之间的关系。
1.元素与集合的关系
元素与集合就一种关系,要么属于某个集合,要么不属于。
- >>> aset
- set(['h', 'o', 'n', 'p', 't', 'y'])
- >>> "a" in aset
- False
- >>> "h" in aset
2.集合与集合的关系
假设两个集合A、B
(1)A是否等于B,即两个集合的元素是否完全一样。
在交互模式下实验
- >>> a
- set(['q', 'i', 's', 'r', 'w'])
- >>> b
- set(['a', 'q', 'i', 'l', 'o'])
- >>> a == b
- False
- >>> a != b
(2)A是否是B的子集,或者反过来,B是否是A的超集,即A的元素是否也都是B的元素,且B的元素比A的元素数量多。
判断集合A是否是集合B的子集,可以使用A<B,返回True则是子集,否则不是。另外,还可以使用函数A.issubset(B)判断。
- >>> a
- set(['q', 'i', 's', 'r', 'w'])
- >>> c
- set(['q', 'i'])
- >>> c < a #c是a的子集
- True
- >>> c.issubset(a) #或者用这种方法,判断c是否是a的子集
- True
- >>> a.issuperset(c) #判断a是否是c的超集
- True
- >>> b
- set(['a', 'q', 'i', 'l', 'o'])
- >>> a < b #a不是b的子集
- False
- >>> a.issubset(b)
- False
(3)A、B的并集,即A、B所有元素,如图1-8所示。
图1-8 A、B的并集
可以使用的符号是“|”,是一个半角状态下的竖线,输入方法是在英文状态下,按下“shift”加上右方括号右边的那个键。表达式是A|B也可使用函数A.union(B),得到的结果就是两个集合并集,注意,这个结果是新生成的一个对象,不是将结合A扩充。
- >>> a
- set(['q', 'i', 's', 'r', 'w'])
- >>> b
- set(['a', 'q', 'i', 'l', 'o'])
- >>> a | b #可以有两种方式,结果一样
- set(['a', 'i', 'l', 'o', 'q', 's', 'r', 'w'])
- >>> a.union(b)
- set(['a', 'i', 'l', 'o', 'q', 's', 'r', 'w'])
(4)A、B的交集,即A、B所公有的元素,如图1-9所示。
图1-9 A、B的交集
- >>> a
- set(['q', 'i', 's', 'r', 'w'])
- >>> b
- set(['a', 'q', 'i', 'l', 'o'])
- >>> a & b #两种方式,等价
- set(['q', 'i'])
- >>> a.intersection(b)
- set(['q', 'i'])
我在实验的时候,顺手敲了下面的代码,出现的结果如下,读者能解释一下吗?
- >>> a and b
- set(['a', 'q', 'i', 'l', 'o'])
(5)A相对B的差(补),即A相对B不同的部分元素,如图1-10所示。
图1-10 A相对B的差(补)
- >>> a
- set(['q', 'i', 's', 'r', 'w'])
- >>> b
- set(['a', 'q', 'i', 'l', 'o'])
- >>> a - b
- set(['s', 'r', 'w'])
- >>> a.difference(b)
- set(['s', 'r', 'w'])
(6)A、B的对称差集,如图1-11所示。
图1-11 A、B的对称差集
- >>> a
- set(['q', 'i', 's', 'r', 'w'])
- >>> b
- set(['a', 'q', 'i', 'l', 'o'])
- >>> a.symmetric_difference(b)
- set(['a', 'l', 'o', 's', 'r', 'w'])
以上是集合的基本运算。在编程中如果用到,可以用前面说的方法查找。