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的方法:

  1. >>> s1 = set("qiwsir")
  2. >>> s1
  3. set(['q', 'i', 's', 'r', 'w'])

把字符串中的字符拆解开形成了集合。特别注意观察:qiwsir中有两个i,但是在s1中只有一个i,也就是集合中元素不能重复。

  1. >>> s2 = set([123, "google", "face", "book", "facebook", "book"])
  2. >>> s2
  3. set(['facebook', 123, 'google', 'book', 'face'])

在创建集合的时候,如果发现了重复的元素,就会过滤一下,剩下不重复的。而且,从s2的创建可以看出,查看结果时显示的元素排列顺序与开始建立时不同,完全是随意显示的(怎么能说明是随机的呢?读者有没有办法?),这说明集合中的元素没有序列。

  1. >>> s3 = {"facebook", 123} #通过{}直接创建
  2. >>> s3
  3. set([123, 'facebook'])

除了用set()来创建集合,还可以使用{}的方式,但是这种方式不提倡使用,因为在某些情况下,Python搞不清楚是字典还是集合。看看下面的探讨就发现问题了。

  1. >>> s3 = {"facebook", [1,2,'a'], {"name":"python", "lang":"english"}, 123}
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. TypeError: unhashable type: 'dict'
  5.  
  6. >>> s3 = {"facebook", [1,2], 123}
  7. Traceback (most recent call last):
  8. File "<stdin>", line 1, in <module>
  9. TypeError: unhashable type: 'list'

从上述实验可以看出,通过{}无法创建含有列表或者字典类型对象元素的集合。

认真阅读报错信息,有这样的词汇:“unhashable”,在理解这个词之前,先看它的反义词“hashable”,很多时候翻译为“可哈希”,其实它有一个不是音译的名词“散列”。如果我们简单点理解,某数据“不可哈希”(unhashable)就是其可变,如列表和字典都能原地修改,就是unhashable。否则,不可变的,类似字符串那样不能原地修改的就是hashable(可哈希)。

对于字典类型的对象,其“键”必须是hashable,即不可变。

现在遇到的集合,其元素也是“可哈希”的。上面的例子,试图将字典、列表作为元素的元素,就报错了。而且报错信息中明确告知列表和字典是不可哈希类型,言外之意,里面的元素都应该是可哈希类型。

继续探索另外一种情况:

  1. >>> s1
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> s1[1] = "I"
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. TypeError: 'set' object does not support item assignment

这里报错,进一步说明集合不是序列类型,不能用索引方式对其进行修改。

根据前面的经验,类型名称函数能够实现类型转换,比如str()就是将对象转化为字符串,同理,分别用list()和set()能够实现集合和列表两种对象之间的转化。

  1. >>> s1
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> lst = list(s1)
  4. >>> lst
  5. ['q', 'i', 's', 'r', 'w']
  6. >>> lst[1] = "I"
  7. >>> lst
  8. ['q', 'I', 's', 'r', 'w']

特别说明,利用set()建立起来的集合是可变集合,可变集合都是unhashable类型的。

1.11.2 集合的函数

把与集合有关的函数找出来。

  1. >>> dir(set)
  2. ['__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']

为了看清楚,我把双画线“__”先删除掉:

  1. '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

  1. >>> help(set.add)
  2.  
  3. Help on method_descriptor:
  4.  
  5. add(...)
  6. Add an element to a set.
  7. This has no effect if the element is already present.

在交互模式中,可以看到:

  1. >>> a_set = {} #我想当然地认为这样也可以建立一个set
  2. >>> a_set.add("qiwsir") #报错!看错误信息。
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. AttributeError: 'dict' object has no attribute 'add'
  6. >>> type(a_set) #Python认为我建立的是一个dict
  7. <type 'dict'>

{}这个东西在dict和set中都有用,但是,若按照上面的方法则建立的是dict,而不是set。这是Python规定的,要建立set,只能用前面已经看到的创建方法了。

  1. >>> a_set = {'a','i'}
  2. >>> type(a_set)
  3. <type 'set'>
  4.  
  5. >>> a_set.add("qiwsir") #增加一个元素
  6. >>> a_set #原地修改
  7. set(['i', 'a', 'qiwsir'])
  8.  
  9. >>> b_set = set("python")
  10. >>> type(b_set)
  11. <type 'set'>
  12. >>> b_set
  13. set(['h', 'o', 'n', 'p', 't', 'y'])
  14. >>> b_set.add("qiwsir")
  15. >>> b_set
  16. set(['h', 'o', 'n', 'p', 't', 'qiwsir', 'y'])
  17.  
  18. >>> b_set.add([1,2,3]) #列表是不可哈希的,集合中的元素应该是hashable类型。
  19. Traceback (most recent call last):
  20. File "<stdin>", line 1, in <module>
  21. TypeError: unhashable type: 'list'
  22.  
  23. >>> b_set.add('[1,2,3]') #可以这样!
  24. >>> b_set
  25. set(['[1,2,3]', 'h', 'o', 'n', 'p', 't', 'qiwsir', 'y'])

除了add()之外,还能够从另外一个集合中合并过来元素,方法是set.update(s2)。

  1. >>> help(set.update)
  2. update(...)
  3. Update a set with the union of itself and others.
  4.  
  5. >>> s1
  6. set(['a', 'b'])
  7. >>> s2
  8. set(['github', 'qiwsir'])
  9. >>> s1.update(s2)
  10. >>> s1
  11. set(['a', 'qiwsir', 'b', 'github'])
  12. >>> s2
  13. set(['github', 'qiwsir'])

2.pop,remove,discard,clear

  1. >>> help(set.pop)
  2. pop(...)
  3. Remove and return an arbitrary set element.
  4. Raises KeyError if the set is empty.
  5.  
  6. >>> b_set
  7. set(['[1,2,3]', 'h', 'o', 'n', 'p', 't', 'qiwsir', 'y'])
  8. >>> b_set.pop() #从set中任意选一个删除,并返回该值
  9. '[1,2,3]'
  10. >>> b_set.pop()
  11. 'h'
  12. >>> b_set.pop()
  13. 'o'
  14. >>> b_set
  15. set(['n', 'p', 't', 'qiwsir', 'y'])
  16.  
  17. >>> b_set.pop("n") #如果要指定删除某个元素就报错了
  18. Traceback (most recent call last):
  19. File "<stdin>", line 1, in <module>
  20. TypeError: pop() takes no arguments (1 given)

set.pop()是从集合中随机选一个元素删除并将这个值返回,但是不能指定删除某个元素。报错信息告诉我们,pop()不能有参数。此外,如果集合是空的了,再做pop()操作也报错。

但是否可以删除指定元素?如果可以,怎么办?

  1. >>> help(set.remove)
  2.  
  3. remove(...)
  4. Remove an element from a set; it must be a member.
  5. If the element is not a member, raise a KeyError.

会有办法的,那么多聪明人早就为读者想好了一个函数——remove()——set.remove(obj)中的obj,必须是set中的元素,否则就报错。

  1. >>> a_set
  2. set(['i', 'a', 'qiwsir'])
  3. >>> a_set.remove("i")
  4. >>> a_set
  5. set(['a', 'qiwsir'])
  6. >>> a_set.remove("w")
  7. Traceback (most recent call last):
  8. File "<stdin>", line 1, in <module>
  9. KeyError: 'w'

跟remove(obj)类似的还有discard(obj):

  1. >>> help(set.discard)
  2.  
  3. discard(...)
  4. Remove an element from a set if it is a member.
  5. If the element is not a member, do nothing.

与help(set.remove)进行信息对比,看看有什么不同。关键在于如果discard(obj)中的obj是set中的元素就删除,如果不是,就什么也不做。新闻就要对比着看才有意思,这里也一样。

  1. >>> a_set.discard('a')
  2. >>> a_set
  3. set(['qiwsir'])
  4. >>> a_set.discard('b')
  5. >>>

在删除上还有一个绝杀,就是set.clear(),它的功能是:Remove all elements from this set.(自己在交互模式下help(set.clear))。

  1. >>> a_set
  2. set(['qiwsir'])
  3. >>> a_set.clear()
  4. >>> a_set
  5. set([])
  6. >>> bool(a_set) #空了,bool一下返回False.
  7. False

1.11.3 补充知识

集合也是一个数学概念(以下定义来自维基百科):

最简单的说法是最原始的集合论——朴素集合论中的定义,集合就是“一堆东西”。集合里的“东西”叫作元素。若然x是集合A的元素,记作x∈A。

集合是现代数学中一个重要的基本概念。集合论的基本理论直到19世纪末才被创立,现在已经是数学教育中一个普遍存在的部分,在小学时就开始学习了。这里对被数学家们称为“直观的”或“朴素的”集合论进行一个简短而基本的介绍;更详细的分析可见朴素集合论。对集合进行严格的公理推导可见公理化集合论。

在计算机中集合是什么呢?自维基百科是这么说的:

在计算机科学中,集合是一组可变数量的数据项(也可能是0个)的组合,这些数据项可能共享某些特征,需要以某种操作方式一起进行操作。一般来讲,这些数据项的类型是相同的,或基类相同(若使用的语言支持继承)。列表(或数组)通常不被认为是集合,因为其大小固定,但事实上它常常在实现中作为某些形式的集合使用。

集合的种类包括列表、集、多重集、树和图。枚举类型可以是列表或集。

1.11.4 不变的集合

以set()创立的集合都是可原地修改的集合,或者说是可变的,也可以说是unhashable。

还有一种集合不能原地修改,这种集合的创建方法是用frozenset(),顾名思义,这是一个被“冻结”的集合,当然是不能修改的,这种集合就是hashable类型——可哈希。

  1. >>> f_set = frozenset("qiwsir")
  2. >>> f_set
  3. frozenset(['q', 'i', 's', 'r', 'w'])
  4. >>> f_set.add("python") #报错,不能修改
  5. Traceback (most recent call last):
  6. File "<stdin>", line 1, in <module>
  7. AttributeError: 'frozenset' object has no attribute 'add'
  8.  
  9. >>> a_set = set("github") #对比看一看
  10. >>> a_set
  11. set(['b', 'g', 'i', 'h', 'u', 't'])
  12. >>> a_set.add("python")
  13. >>> a_set
  14. set(['b', 'g', 'i', 'h', 'python', 'u', 't'])

1.11.5 集合运算

唤醒中学数学(准确说是高中数学中的一点知识)中关于集合的知识,当然,如果你是某个理工科的专业大学毕业,应该更熟悉集合之间的关系。

1.元素与集合的关系

元素与集合就一种关系,要么属于某个集合,要么不属于。

  1. >>> aset
  2. set(['h', 'o', 'n', 'p', 't', 'y'])
  3. >>> "a" in aset
  4. False
  5. >>> "h" in aset

2.集合与集合的关系

假设两个集合A、B

(1)A是否等于B,即两个集合的元素是否完全一样。

在交互模式下实验

  1. >>> a
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> b
  4. set(['a', 'q', 'i', 'l', 'o'])
  5. >>> a == b
  6. False
  7. >>> a != b

(2)A是否是B的子集,或者反过来,B是否是A的超集,即A的元素是否也都是B的元素,且B的元素比A的元素数量多。

判断集合A是否是集合B的子集,可以使用A<B,返回True则是子集,否则不是。另外,还可以使用函数A.issubset(B)判断。

  1. >>> a
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> c
  4. set(['q', 'i'])
  5. >>> c < a #c是a的子集
  6. True
  7. >>> c.issubset(a) #或者用这种方法,判断c是否是a的子集
  8. True
  9. >>> a.issuperset(c) #判断a是否是c的超集
  10. True
  11.  
  12. >>> b
  13. set(['a', 'q', 'i', 'l', 'o'])
  14. >>> a < b #a不是b的子集
  15. False
  16. >>> a.issubset(b)
  17. False

(3)A、B的并集,即A、B所有元素,如图1-8所示。

1.11 集合 - 图1图1-8 A、B的并集

可以使用的符号是“|”,是一个半角状态下的竖线,输入方法是在英文状态下,按下“shift”加上右方括号右边的那个键。表达式是A|B也可使用函数A.union(B),得到的结果就是两个集合并集,注意,这个结果是新生成的一个对象,不是将结合A扩充。

  1. >>> a
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> b
  4. set(['a', 'q', 'i', 'l', 'o'])
  5. >>> a | b #可以有两种方式,结果一样
  6. set(['a', 'i', 'l', 'o', 'q', 's', 'r', 'w'])
  7. >>> a.union(b)
  8. set(['a', 'i', 'l', 'o', 'q', 's', 'r', 'w'])

(4)A、B的交集,即A、B所公有的元素,如图1-9所示。

1.11 集合 - 图2图1-9 A、B的交集

  1. >>> a
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> b
  4. set(['a', 'q', 'i', 'l', 'o'])
  5. >>> a & b #两种方式,等价
  6. set(['q', 'i'])
  7. >>> a.intersection(b)
  8. set(['q', 'i'])

我在实验的时候,顺手敲了下面的代码,出现的结果如下,读者能解释一下吗?

  1. >>> a and b
  2. set(['a', 'q', 'i', 'l', 'o'])

(5)A相对B的差(补),即A相对B不同的部分元素,如图1-10所示。

1.11 集合 - 图3图1-10 A相对B的差(补)

  1. >>> a
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> b
  4. set(['a', 'q', 'i', 'l', 'o'])
  5. >>> a - b
  6. set(['s', 'r', 'w'])
  7. >>> a.difference(b)
  8. set(['s', 'r', 'w'])

(6)A、B的对称差集,如图1-11所示。

1.11 集合 - 图4图1-11 A、B的对称差集

  1. >>> a
  2. set(['q', 'i', 's', 'r', 'w'])
  3. >>> b
  4. set(['a', 'q', 'i', 'l', 'o'])
  5. >>> a.symmetric_difference(b)
  6. set(['a', 'l', 'o', 's', 'r', 'w'])

以上是集合的基本运算。在编程中如果用到,可以用前面说的方法查找。