Python中set实现去重的原理、类的魔术方法总结以及应用

1. 集合(set)元素去重的判断依据是什么

集合中元素去重的依据是集合中包含的对象具有相同的hash值,同时被去重对象的等值比较结果为True。集合中包含的对象不能存在2个具有相同hash值且等值比较为True的对象。所以,当向集合中加入的对象具有相同的hash值的时候,如果两个对象的等值比较为False,则两个对象可以同时出现在集合中;如果两个对象具有相同的hash值,同时等值比较结果为True,那么此时就会被去重,只能保留下1个。

关于上述论断,证明代码如下所示:

x = 'Tim Anderson'
y = 'Tim Anderson'

s1 = {x, y}
print('集合1中包含的元素为:', s1)
print('字符串x的hash值为:', hash(x))
print('字符串y的hash值为:', hash(y))

print(id(x), id(y), id(x) == id(y), x == y)

print('*' * 30)

class Person:
	def __init__(self, name):
    	self.name = name

	# def __eq__(self, other):
	#     return self.name == other.name     # 此时p1 == p2,集合s2中只包含1个对象
	def __eq__(self, other):
    	return id(self) == id(other)    # 此时p1 != p2,集合s2中包含2个对象

	def __hash__(self):
    	return hash(self.name)   # 如果直接写为hash(self)则会导致递归

p1 = Person('Tim Anderson')
p2 = Person('Tim Anderson')

print('p1 == p2?', p1 == p2)

s2 = {p1, p2}
print('集合2中包含的元素为:', s2)
print('Person对象p1的hash值为:', hash(p1))
print('Person对象p2的hash值为:', hash(p2))

上述代码的执行结果如下所示:

集合1中包含的元素为: {'Tim Anderson'}
字符串x的hash值为: 3480703692780143279
字符串y的hash值为: 3480703692780143279
2413477346864 2413477345712 False True
******************************
p1 == p2? False
集合2中包含的元素为: {<__main__.Person object at 0x00000231EE755788>, <__main__.Person object at 0x00000231EE42DD88>}
Person对象p1的hash值为: 3480703692780143279
Person对象p2的hash值为: 3480703692780143279

示例代码中可以看出,hash值相同并不是去重的充要条件,是必要不充分条件。即去重的两个对象一定具有相同的hash值,比如上面示例代码中的字符串x和y;但是当两个对象的hash值相同,但是并不相等的时候,则并不会对这两个对象执行去重操作,比如上面示例代码中的Person类的实例对象p1和p2虽然具有相同的hash值,但是并没有被去重,因为它们并不相等。

所以从上述两个示例的对比中,可以看出集合去重的充分必要条件是如下两条:

  1. hash值相同
  2. 等值比较结果为True。

所以,要实现集合或者字典的key的类似功能,就需要实现魔术方法__hash__, __eq__,前者用于指定hash值的计算目标;后者用于指定对象等值比较的计算目标。

上述示例代码就是集合去重功能的演示,以及背后的原理。

2. 总结魔术方法的作用及什么情况会执行到该方法

Python中提供了一些魔术方法,以便完成特定的操作。下面将这些魔术方法归类整理如下:

2.1. 构造和初始化实例对象相关的魔术方法

该类包含的魔术方法有三个:

  1. __new__(cls, ...):该方法是创建类的实例对象的时候,第一个被调用的方法,创建实例对象的类作为其第一个参数,后续的其他参数,都会作为参数传递给__init__魔术方法,从而实现新创建的实例对象的初始化操作。__new__魔术方法在实际中使用的较少,但在一些特定的场景下,还是会被用到的,比如子类是tuple类型或者string类型的时候。
  2. __init__(self, ...):该方法用于初始化类的实例对象,这个方法在类的实现上,基本总是会被用到。
  3. __del__(self):如果__new__, __init__这两个方法被称为是构造方法的话,那么这个__del__方法就被称为是析构方法,当del语句的操作对象是该类的实例对象的时候,会自动调用该方法。

Python中创建类的实例对象的时候,会最先调用__new__方法,然后才会调用__init__方法。

2.2. 打印相关的魔术方法

有的时候,需要将类或者其实例对象以字符串的形式呈现出来,此时就需要实现这个类别下的相关魔术方法:

  1. __str__(self):该方法用于定义str()内建函数以该类的实例对象作为参数的时候,执行的相应操作,通常用于直接打印实例对象。

  2. __repr__(self):该方法用于定义repr()内建函数以该类的实例对象作为参数的时候,执行的操作。__str__方法与__repr__方法的区别在于,前者的操作对象是直接的该类的实例对象;后者的操作对象是将该类的实例对象放在列表或者元组之类的容器中进行打印的时候,才会调用。

    比如,有下面的一个类,以及类的实例对象:

    class A:
     def __init__(self, name):
         self.name = name
    
    a = A('Tim Anderson')
    b = A('John Bon Jovi')
    
    print('obj a = {}'.format(a))		# 此时调用__str__魔术方法
    
    print('the set of obj a and b is : {}'.format({a, b}))   # 此时调用__repr__魔术方法,因为此时a, b被包含在集合这个容器中。
    
  3. __unicode__(self):该方法用于定义unicode()内建函数以该类的实例对象作为参数的时候,执行的具体操作。unicode函数的效果类似于str,但是其返回Unicode编码的字符串。

  4. __format__(self, formatstr):该方法用于定义format()内建函数对该类的实例对象进行格式化打印的时候执行的操作。比如'Hello, 0:abc!'.format(a)就会触发a.__format("abc")的魔术方法调用。

  5. __hash__(self):用于定义hash()函数以该类的实例对象作为参数的时候,执行的操作。通常返回一串整数,通常用于在集合以及字典中进行快速的可哈希对象比较。为了实现集合中的去重以及字典中的key去重的功能,通常还需要实现__eq__魔术方法,通过这两个方法,就可以实现类似集合或者字典一样的自定义实例。

  6. __bool__(self):用于定义bool()函数调用的时候所执行的操作,返回的结果为布尔值。如果类中没有实现这个魔术方法,那么就会转而调用__len__魔术方法。如果这两个方法都没有实现,则该类的所有实例对象都返回True。

  7. __dir__(self):定义dir()内建函数以该类的实例对象作为参数的时候,执行的操作。通常返回属性列表。

2.3. 运算相关的魔术方法

对于数学运算相关的魔术方法,Python中提供了比较运算、算术运算、反射的算术运算、增量赋值、类型转换等相关的魔术方法。具体如下。

2.3.1 关系比较运算相关的魔术方法

  1. __eq__(self, other):对应的运算符为==
  2. __ne__(self, other):对应的运算符为!=
  3. __lt__(self, other):对应的运算符为<
  4. __gt__(self, other):对应的运算符为>
  5. __le__(self, other):对应的运算符为<=
  6. __ge__(self, other):对应的运算符为>=

2.3.2. 算数相关的魔术方法

这个类别下包含5个部分:单目运算符、常规数学运算符、反射数学运算符、增量赋值运算符以及类型转换。

2.3.2.1. 单目运算符
  1. __pos__(self):表示正数,比如+obj
  2. __neg__(self):表示负数,比如-obj
  3. __abs__(self):取绝对值,当abs()函数以该实例对象作为参数的时候,调用该方法
  4. __invert__(self):对应的操作符为~
  5. __round(self, n):四舍五入,round()函数以该对象作为参数的时候,调用该方法
  6. __floor__(self):向下取整,对应的方法为:math.floor()
  7. __ceil__(self):向上取整,对应的方法为:math.ceil()
  8. __trunc__(self):截断取整,对应的方法为:math.trunc()
2.3.2.2. 常规数学运算符

涉及的魔术方法如下:

  1. __add__(self, other):对应加法运算,+
  2. __sub__(self, other):对应减法运算,-
  3. __mul__(self, other):对应乘法运算,*
  4. __div__(self, other):对应除法运算,/
  5. __mod__(self, other):对应取模运算,%
  6. __pow__(self, other):对应平方运算,**
  7. __lshift__(self, other):对应循环左移位运算,<<
  8. __rshift__(self, other):对应循环右移位运算,>>
  9. __and__(self, other):对应逻辑与运算,&
  10. __or__(self, other):对应逻辑或运算,|
  11. __xor__(self, other):对应逻辑异或运算,^
2.3.2.3. 反射数学运算符

在正常的类的实例方法中,以加法运算为例,当执行obj + other的时候,调用的是__add__(self, other)这个魔术方法,但是当计算other + obj的时候,此时__add__(self, other)方法就无法满足要求了。此时就需要用到反射数学运算符了。具体如下所示:

  1. __radd__(self, other):执行效果类似于__add__魔术方法,但是该类实例对象在运算符右边
  2. __rsub__(self, other):执行效果类似于__sub__魔术方法,但是该类实例对象在运算符右边
  3. __rmul__(self, other):执行效果类似于__mul__魔术方法,但是该类实例对象在运算符右边
  4. __rdiv__(self, other):执行效果类似于__div__魔术方法,但是该类实例对象在运算符右边
  5. __rmod__(self, other):执行效果类似于__mod__魔术方法,但是该类实例对象在运算符右边
  6. __rpow__(self, other):执行效果类似于__pow__魔术方法,但是该类实例对象在运算符右边
  7. __rlshift__(self, other):执行效果类似于__lshift__魔术方法,但是该类实例对象在运算符右边
  8. __rrshift__(self, other):执行效果类似于__rshift__魔术方法,但是该类实例对象在运算符右边
  9. __rand__(self, other):执行效果类似于__and__魔术方法,但是该类实例对象在运算符右边
  10. __ror__(self, other):执行效果类似于__or__魔术方法,但是该类实例对象在运算符右边
  11. __rxor__(self, other):执行效果类似于__xor__魔术方法,但是该类实例对象在运算符右边
2.3.2.4. 增量赋值运算符

增量赋值运算符包含的魔术方法如下所示:

  1. __iadd__(self, other):对应的运算符为+=
  2. __isub__(self, other):对应的运算符为-=
  3. __imul__(self, other):对应的运算符为*=
  4. __idiv__(self, other):对应的运算符为/=
  5. __imod__(self, other):对应的运算符为%=
  6. __ipow__(self, other):对应的运算符为**=
  7. __ilshift__(self, other):对应的运算符为<<=
  8. __irshift__(self, other):对应的运算符为>>=
  9. __iand__(self, other):对应的运算符为&=
  10. __ior__(self, other):对应的运算符为|=
  11. __ixor__(self, other):对应的运算符为^=
2.3.2.5. 类型转换

该类别下包含的魔术方法如下所示:

  1. __int__(self):对应于int()内建函数
  2. __oct__(self):对应于oct()内建函数
  3. __hex__(self):对应于hex()内建函数

2.4. 属性控制相关的魔术方法

该类主要包含的魔术方法有4个:

  1. __getattr__(self, name):用于访问实例对象中的属性,对应getattr(obj, name, default)方法
  2. __setattr__(self, name, value):对应setattr(obj, name, value)方法,用于设置实例对象的属性
  3. __delattr__(self, name):用于删除实例对象的属性
  4. __getattribute__(self, name):同样用于访问实例对象中的属性,如果定义了该魔术方法,则最先调用,找不到相关属性,才会转而调用__getattr__魔术方法,如果仍然找不到,则抛出AttributeError异常。

2.5. 容器相关的魔术方法

包含的魔术方法如下所示:

  1. __len__(self):返回实例对象的长度
  2. __getitem__(self, key):从实例对象中返回键为key的值
  3. __setitem__(self, key, value):将实例对象的key设置为value
  4. __delitem__(self, key):删除实例对象中属性名为key的对应值
  5. __iter__(self):返回该实例对象的迭代器
  6. __reversed__(self):对应于reversed()内建方法
  7. __contains__(self, item):对应成员关系运算,in或者not in操作
  8. __missing__(self, key):通常应用于dict的子类中,当指定的键key缺失的时候,调用该魔术方法

2.6. 属性描述器

属性描述器对应的魔术方法包含三个:

  1. __get__(self, instance, owner):其中self为属性描述器类的实例对象,instance为包含属性描述器类作为类属性的属主类的实例对象,owner表示包含属性描述器类作为类属性的属主类自身
  2. __set__(self, instance, value):同上
  3. __delete__(self, instance):同上

2.7. 可调用对象与上下文管理

这部分也包含三个魔术方法:

  1. __call__(self, [args...]):实现这个魔术方法,可以使实例对象像函数一样被调用
  2. __enter__(self):用于实现上下文管理协议,在with语句头中开始执行
  3. __exit__(self):用于实现实现上下文管理协议,在结束上下文环境的时候执行

2.8. 拷贝

这个部分包含2个魔术方法:

  1. __copy__(self):用于实现浅拷贝
  2. __deepcopy__(self, memodict):用于实现深拷贝

3. 利用魔术方法实现如下类

要求:

定义一个名为Ob的类,使该类经过如下操作(第5-14行的代码)之后,得到最终期望的结果(第16-20行的内容)。

class Ob:
    pass

执行以下操作:
a = Ob('tom')
b = Ob('tom')
print('a: ', a)
a[0] = 'a'
print('a:', a)
a * 3
print('len: ', len(a))
del a[1]
print('a: ', a)
print('set: ', {a, b})
其输出如下:
a: ['1', '2']
a: ['a', '2']
len:  6
a: ['a', 'a', '2', 'a', '2']
set: {<Ob name=tom items=['a', 'a', '2', 'a', '2']>}

分析:

  1. 上述代码框中的第5、6行为类实例化,所以需要在Ob类中实现__init__(self, name)
  2. 上述代码框中的第7行和第9行以及第13行,打印Ob类的实例对象,所以需要在Ob类中实现__str__(self)
  3. 上述代码框中的第8行为实例对象通过索引赋值,此时就需要实现__getitem__(self, idx), __setitem__(self, idx, val)
  4. 上述代码框中的第10行将Ob类的实例对象与整数相乘,最终得到一个列表,所以需要实现__mul__(self, coe)
  5. 上述代码框中的第11行中使用了len内建函数求取实例对象的长度,此时就需要实现__len__(self)
  6. 上述代码框中的第12行通过索引删除实例对象中的一个元素,此时就需要实现__delitem__(self, idx)
  7. 上述代码框中的第14行,将Ob类的两个实例对象放在容器类型集合中进行打印,此时就需要实现__repr__(self)方法,另外,由于集合具有去重的功能,且其中存储的实例对象需要是可哈希的对象。为了确保实例对象是可哈希的,所以就需要实现__hash__(self)方法;而要保证集合的去重功能,就需要实现__len__(self)方法,即两个对象相同的时候,就会执行去重操作。

上述就是要得到第16-20行的期望结果所需要实现的全部魔术方法。

实现:

符合上述要求的代码实现如下所示:

# encoding = utf-8


"""
class Ob:
 pass

执行以下操作:
# __init__(self, name)
a = Ob('tom')   # list
b = Ob('tom')

# __str__(self)
print('a: ', a)     # a: ['1', '2']

# __setitem__(self, idx, val)
a[0] = 'a'
print('a:', a)      # a: ['a', '2']

# __mul__(self, coe)
a * 3   # a = ['a', '2', 'a', '2', 'a', '2']

# __len__(self)
print('len: ', len(a))   # len: 6

# __delitem__(self)  __getitem__(self, idx)
del a[1]
print('a: ', a)     # a: ['a', 'a', '2', 'a', '2']
print('set: ', {a, b})  # __repr__(self) __hash__(self) __eq__(self, other)


其输出结果如下:
a: ['1', '2']
a: ['a', '2']
len: 6
a: ['a', 'a', '2', 'a', '2']
set: {<Ob name=tom items=['a', 'a', '2', 'a', '2']>}
"""


class Ob:
	__count = 0
	__count_lst = []

	def __init__(self, name):
    	self.name = name
	    Ob.__count += 1
    	Ob.__count_lst.append(str(Ob.__count))

	def __str__(self):
    	return str(Ob.__count_lst)

	def __repr__(self):
    	return '<{} name={} items={}>'.format(
        	Ob.__name__,
	        self.name,
    	    Ob.__count_lst
	    )
    	pass

	def __getitem__(self, idx):
    	# lth = len(self.__count_lst)
	    lth = len(self)
    	# if idx < lth and idx >= -lth:
	    if lth > idx >= -lth:   # 支持使用链式比较
    	    return Ob.__count_lst[idx]
	    else:
    	    raise IndexError('Index {} is out of the valid range'.format(idx))

	def __setitem__(self, idx, val):
    	lth = len(self)
	    if lth > idx >= -lth:
    	    Ob.__count_lst[idx] = val
	    else:
    	    raise IndexError('Index {} is out of the valid range'.format(idx))

	def __delitem__(self, idx):
    	lth = len(self)
	    if lth > idx >= -lth:
    	    del Ob.__count_lst[idx]

	def __len__(self):
    	return len(Ob.__count_lst)

	def __mul__(self, coe):
    	if isinstance(coe, int):
        	Ob.__count_lst *= 3
	    return self

	# 下面两个魔术方法用于实现最后的集合功能,即去重
	def __hash__(self):
    	return hash(self.name)

	def __eq__(self, other):
    	return self.__count_lst == other.__count_lst


if __name__ == '__main__':
	a = Ob('tom')
	b = Ob('tom')

	print('a:', a)

	a[0] = 'a'
	print('a:', a)

	a * 3
	print('len:', len(a))

	del a[1]
	print('a:', a)

	print('set:', {a, b})

上述代码的执行结果如下所示:

a: ['1', '2']
a: ['a', '2']
len: 6
a: ['a', 'a', '2', 'a', '2']
set: {<Ob name=tom items=['a', 'a', '2', 'a', '2']>}

Process finished with exit code 0

上述就完成了所需要的代码。

4. References

[1]. A Guide to Python’s Magic Methods

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值