還不清楚魔術方法?
可以看看本系列開篇:【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博客