【Python】魔法方法是真的魔法! (第二期)
还不清楚魔术方法?
可以看看本系列开篇:【Python】小子!是魔术方法!-CSDN博客
- 【Python】魔法方法是真的魔法! (第一期)-CSDN博客
在 Python 中,如何自定义数据结构的比较逻辑?除了等于和不等于,其他rich comparison
操作符如何实现以及是否对称?
在 Python 中,如果想自定义数据结构(如自定义日期类)的比较逻辑,可以通过魔术方法实现。例如:
- 通过定义
__eq__
函数来改变默认的"等于"比较行为 - 不等于运算符可以通过定义
__ne__
函数来自定义逻辑 - 对于大于、小于等操作符需要定义
__gt__
、__lt__
等方法
class MyDate:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __eq__(self, other):if not isinstance(other, MyDate):return NotImplemented # 或者 False,取决于你的需求return (self.year, self.month, self.day) == (other.year, other.month, other.day)def __ne__(self, other):# 通常不需要定义 __ne__,Python 会自动取 __eq__ 的反# 但如果需要特殊逻辑,可以像下面这样定义if not isinstance(other, MyDate):return NotImplementedreturn not self.__eq__(other)def __lt__(self, other):if not isinstance(other, MyDate):return NotImplementedreturn (self.year, self.month, self.day) < (other.year, other.month, other.day)def __le__(self, other):if not isinstance(other, MyDate):return NotImplementedreturn (self.year, self.month, self.day) <= (other.year, other.month, other.day)def __gt__(self, other):if not isinstance(other, MyDate):return NotImplementedreturn (self.year, self.month, self.day) > (other.year, other.month, other.day)def __ge__(self, other):if not isinstance(other, MyDate):return NotImplementedreturn (self.year, self.month, self.day) >= (other.year, other.month, other.day)# 示例
date1 = MyDate(2023, 10, 26)
date2 = MyDate(2023, 10, 26)
date3 = MyDate(2023, 11, 1)print(f"date1 == date2: {date1 == date2}") # True
print(f"date1 != date3: {date1 != date3}") # True
print(f"date1 < date3: {date1 < date3}") # True
print(f"date3 > date1: {date3 > date1}") # True
注意:
- 通常只需定义
__eq__
,因为__ne__
默认会取__eq__
的反结果 rich comparison
操作符在没有自定义时会抛出错误- 比较不同类对象时,会优先调用子类的方法
在实现rich comparison
时,如果两个对象不是同一类,会如何处理?
当进行rich comparison
时:
- 如果 Y 是 X 的子类,优先使用 Y 的比较方法
- 否则优先使用 X 的比较方法
这意味着不同类对象的比较可能触发不同的比较逻辑
class Fruit:def __init__(self, name, weight):self.name = nameself.weight = weightdef __eq__(self, other):print("Fruit __eq__ called")if not isinstance(other, Fruit):return NotImplementedreturn self.name == other.name and self.weight == other.weightdef __lt__(self, other):print("Fruit __lt__ called")if not isinstance(other, Fruit):return NotImplementedreturn self.weight < other.weightclass Apple(Fruit):def __init__(self, name, weight, color):super().__init__(name, weight)self.color = colordef __eq__(self, other):print("Apple __eq__ called")if not isinstance(other, Apple):# 如果对方不是Apple,但可能是Fruit,可以委托给父类if isinstance(other, Fruit):return super().__eq__(other) # 或者自定义不同的逻辑return NotImplementedreturn super().__eq__(other) and self.color == other.colordef __lt__(self, other):print("Apple __lt__ called")if not isinstance(other, Apple):if isinstance(other, Fruit): # 与Fruit比较权重return self.weight < other.weightreturn NotImplementedreturn self.weight < other.weight # 假设苹果之间也按重量比较apple1 = Apple("Fuji", 150, "red")
apple2 = Apple("Gala", 150, "reddish-yellow")
fruit1 = Fruit("Orange", 170)print(f"apple1 == apple2: {apple1 == apple2}") # Apple __eq__ called (比较 apple1 和 apple2)# Fruit __eq__ called (Apple的__eq__调用了super().__eq__)# 输出: apple1 == apple2: False (因为颜色不同)print(f"apple1 == fruit1: {apple1 == fruit1}") # Apple __eq__ called (apple1是Apple类,优先调用其__eq__)# Fruit __eq__ called (Apple的__eq__中调用super().__eq__)# 输出: apple1 == fruit1: Falseprint(f"fruit1 == apple1: {fruit1 == apple1}") # Fruit __eq__ called (fruit1是Fruit类,优先调用其__eq__)# 输出: fruit1 == apple1: Falseprint(f"apple1 < fruit1: {apple1 < fruit1}") # Apple __lt__ called (apple1是Apple类,优先调用其__lt__)# 输出: apple1 < fruit1: True (150 < 170)print(f"fruit1 < apple1: {fruit1 < apple1}") # Fruit __lt__ called (fruit1是Fruit类,优先调用其__lt__)# 输出: fruit1 < apple1: False (170 < 150 is False)
在 Python 中,如何获取自定义数据结构的hash
值?
- 通过调用
hash(x)
获取默认hash
值 - 自定义对象常用作字典、集合的键
- 注意:Python 不会自动推断
rich comparison
运算关系
class Point:def __init__(self, x, y):self.x = xself.y = y# 未定义 __eq__ 和 __hash__
p1 = Point(1, 2)
p2 = Point(1, 2)print(f"Hash of p1 (default): {hash(p1)}")
print(f"Hash of p2 (default): {hash(p2)}")
print(f"p1 == p2 (default): {p1 == p2}") # False, 因为默认比较的是对象ID# 将对象放入字典或集合
point_set = {p1}
point_set.add(p2)
print(f"Set of points (default hash): {point_set}") # 包含两个不同的 Point 对象point_dict = {p1: "Point 1"}
point_dict[p2] = "Point 2" # p2 被视为新键
print(f"Dictionary of points (default hash): {point_dict}")
在 Python 中,为什么两个相同的自定义对象在字典中会被视为不同的键?
原因:
- 自定义
__eq__
方法后,默认__hash__
会被删除 - 需要同时自定义
__hash__
方法 - 必须保证相等对象具有相同
hash
值
class Coordinate:def __init__(self, lat, lon):self.lat = latself.lon = londef __eq__(self, other):if not isinstance(other, Coordinate):return NotImplementedreturn self.lat == other.lat and self.lon == other.lon# 只定义了 __eq__,没有定义 __hash__
coord1 = Coordinate(10.0, 20.0)
coord2 = Coordinate(10.0, 20.0)print(f"coord1 == coord2: {coord1 == coord2}") # Truetry:# 尝试将对象用作字典的键或放入集合coordinates_set = {coord1}print(coordinates_set)
except TypeError as e:print(f"Error when adding to set: {e}") # unhashable type: 'Coordinate'# 定义 __hash__
class ProperCoordinate:def __init__(self, lat, lon):self.lat = latself.lon = londef __eq__(self, other):if not isinstance(other, ProperCoordinate):return NotImplementedreturn self.lat == other.lat and self.lon == other.londef __hash__(self):# 一个好的实践是使用元组来组合属性的哈希值return hash((self.lat, self.lon))p_coord1 = ProperCoordinate(10.0, 20.0)
p_coord2 = ProperCoordinate(10.0, 20.0)
p_coord3 = ProperCoordinate(30.0, 40.0)print(f"p_coord1 == p_coord2: {p_coord1 == p_coord2}") # True
print(f"hash(p_coord1): {hash(p_coord1)}")
print(f"hash(p_coord2): {hash(p_coord2)}")
print(f"hash(p_coord3): {hash(p_coord3)}")coordinates_map = {p_coord1: "Location A"}
coordinates_map[p_coord2] = "Location B" # p_coord2 会覆盖 p_coord1,因为它们相等且哈希值相同
coordinates_map[p_coord3] = "Location C"print(f"Coordinates map: {coordinates_map}")
# 输出: Coordinates map: {<__main__.ProperCoordinate object at ...>: 'Location B', <__main__.ProperCoordinate object at ...>: 'Location C'}
# 注意:输出的对象内存地址可能不同,但键是根据哈希值和相等性判断的
如何自定义一个合法且高效的hash
函数?
要求:
- 必须返回整数
- 相等对象必须返回相同
hash
值 - 推荐做法:
避免直接返回常数,否则会导致大量哈希冲突def __hash__(self):return hash((self.attr1, self.attr2))
class Book:def __init__(self, title, author, isbn):self.title = titleself.author = authorself.isbn = isbn # 假设 ISBN 是唯一的标识符def __eq__(self, other):if not isinstance(other, Book):return NotImplemented# 通常,如果有一个唯一的ID(如ISBN),仅基于它进行比较就足够了# 但为了演示,我们比较所有属性return (self.title, self.author, self.isbn) == \(other.title, other.author, other.isbn)def __hash__(self):# 好的做法:基于不可变且用于 __eq__ 比较的属性来计算哈希值# 如果 ISBN 是唯一的,且 __eq__ 主要依赖 ISBN,那么可以:# return hash(self.isbn)# 或者,如果所有属性都重要:print(f"Calculating hash for Book: {self.title}")return hash((self.title, self.author, self.isbn))class BadHashBook(Book):def __hash__(self):# 不好的做法:返回常数,会导致大量哈希冲突print(f"Calculating BAD hash for Book: {self.title}")return 1book1 = Book("The Hitchhiker's Guide", "Douglas Adams", "0345391802")
book2 = Book("The Hitchhiker's Guide", "Douglas Adams", "0345391802") # 相同的书
book3 = Book("The Restaurant at the End of the Universe", "Douglas Adams", "0345391810")print(f"book1 == book2: {book1 == book2}") # True
print(f"hash(book1): {hash(book1)}")
print(f"hash(book2): {hash(book2)}") # 应该与 hash(book1) 相同
print(f"hash(book3): {hash(book3)}") # 应该与 hash(book1) 不同book_set = {book1, book2, book3}
print(f"Book set (good hash): {len(book_set)} books") # 应该是 2 本书bad_book1 = BadHashBook("Book A", "Author X", "111")
bad_book2 = BadHashBook("Book B", "Author Y", "222") # 不同的书,但哈希值相同
bad_book3 = BadHashBook("Book C", "Author Z", "333") # 不同的书,但哈希值相同print(f"hash(bad_book1): {hash(bad_book1)}")
print(f"hash(bad_book2): {hash(bad_book2)}")
print(f"hash(bad_book3): {hash(bad_book3)}")# 由于哈希冲突,字典/集合的性能会下降
# 尽管它们仍然能正确工作(因为 __eq__ 会被用来解决冲突)
bad_book_set = {bad_book1, bad_book2, bad_book3}
print(f"Bad book set (bad hash): {len(bad_book_set)} books") # 应该是 3 本书,但查找效率低
# 当插入 bad_book2 时,它的哈希值是 1,与 bad_book1 冲突。
# Python 会接着调用 __eq__ 来区分它们。因为它们不相等,所以 bad_book2 会被添加。
# 对 bad_book3 同理。
如果自定义对象是mutable
的,为什么不应该将其用作字典的key
?
原因:
- 字典基于
hash
值快速访问 - 对象修改后
hash
值可能改变 - 会导致字典检索失效或出错
class MutableKey:def __init__(self, value_list):# 使用列表,这是一个可变类型self.value_list = value_listdef __hash__(self):# 注意:如果列表内容改变,哈希值也会改变# 这使得它不适合做字典的键# 为了能 hash,我们将列表转换为元组return hash(tuple(self.value_list))def __eq__(self, other):if not isinstance(other, MutableKey):return NotImplementedreturn self.value_list == other.value_listdef __repr__(self):return f"MutableKey({self.value_list})"key1 = MutableKey([1, 2])
my_dict = {key1: "Initial Value"}print(f"Dictionary before modification: {my_dict}")
print(f"Value for key1: {my_dict.get(key1)}") # "Initial Value"# 现在修改 key1 内部的可变状态
key1.value_list.append(3)
print(f"Key1 after modification: {key1}") # MutableKey([1, 2, 3])# 尝试用修改后的 key1 (现在是 [1, 2, 3]) 访问字典
# 它的哈希值已经变了
try:print(f"Value for modified key1: {my_dict[key1]}")
except KeyError:print("KeyError: Modified key1 not found in dictionary.")# 尝试用原始状态 ([1, 2]) 的新对象访问
original_key_representation = MutableKey([1, 2])
print(f"Value for original_key_representation: {my_dict.get(original_key_representation)}")
# 输出可能是 None 或 KeyError,因为原始 key1 在字典中的哈希槽是根据 [1,2] 计算的,
# 但 key1 对象本身已经被修改,其 __hash__ 现在会基于 [1,2,3] 计算。
# 字典的内部结构可能已经不一致。# 更糟糕的是,如果哈希值没有改变,但 __eq__ 的结果改变了,也会出问题。# 正确的做法是使用不可变对象作为键,或者确保可变对象在作为键期间不被修改。
# 例如,Python 的内置 list 类型是 unhashable 的:
try:unhashable_dict = {[1,2,3]: "test"}
except TypeError as e:print(f"Error with list as key: {e}") # unhashable type: 'list'
自定义对象在条件判断语句中如何被处理?
默认行为:
- 自定义对象在布尔上下文中被视为
True
自定义方法:
- 重载
__bool__
魔术方法 - 或重载
__len__
方法(返回 0 时为False
)
示例:
class MyCollection:def __init__(self, items=None):self._items = list(items) if items is not None else []self.is_active = True # 一个自定义的布尔状态# __bool__ 优先于 __len__def __bool__(self):print("__bool__ called")return self.is_active and len(self._items) > 0 # 例如,只有激活且非空时为 Truedef __len__(self):print("__len__ called")return len(self._items)# 示例 1: __bool__ 定义了逻辑
collection1 = MyCollection([1, 2, 3])
collection1.is_active = True
if collection1:print("Collection1 is True") # __bool__ called, Collection1 is True
else:print("Collection1 is False")collection2 = MyCollection() # 空集合
collection2.is_active = True
if collection2:print("Collection2 is True")
else:print("Collection2 is False") # __bool__ called, Collection2 is False (因为长度为0)collection3 = MyCollection([1])
collection3.is_active = False # 非激活状态
if collection3:print("Collection3 is True")
else:print("Collection3 is False") # __bool__ called, Collection3 is False (因为 is_active 是 False)class MySizedObject:def __init__(self, size):self.size = size# 没有 __bool__,但有 __len__def __len__(self):print("__len__ called")return self.size# 示例 2: 只有 __len__
sized_obj_non_zero = MySizedObject(5)
if sized_obj_non_zero:print("Sized object (non-zero len) is True") # __len__ called, Sized object (non-zero len) is True
else:print("Sized object (non-zero len) is False")sized_obj_zero = MySizedObject(0)
if sized_obj_zero:print("Sized object (zero len) is True")
else:print("Sized object (zero len) is False") # __len__ called, Sized object (zero len) is False# 示例 3: 既没有 __bool__ 也没有 __len__ (默认行为)
class EmptyShell:passshell = EmptyShell()
if shell:print("EmptyShell object is True by default") # EmptyShell object is True by default
else:print("EmptyShell object is False by default")# def __bool__(self):
# return self.is_valid # 这是笔记中原有的示例,已整合到 MyCollection 中
注意:__bool__
优先于__len__
被调用
第三期
插眼待更
关于作者
- CSDN 大三小白新手菜鸟咸鱼长期更新强烈建议不要关注!
作者的其他文章
Python
- 【Python】装饰器在装什么-CSDN博客
- 【Python】【面试凉经】Fastapi为什么Fast-CSDN博客
- 【Python】小子!是魔术方法!-CSDN博客
- 【Python】一直搞不懂迭代器是个啥。。-CSDN博客