【Python】魔法方法是真的魔法! (第二期)

#新星杯·14天创作挑战营·第11期#

还不清楚魔术方法?
可以看看本系列开篇:【Python】小子!是魔术方法!-CSDN博客

在 Python 中,如何自定义数据结构的比较逻辑?除了等于和不等于,其他rich comparison操作符如何实现以及是否对称?

在 Python 中,如果想自定义数据结构(如自定义日期类)的比较逻辑,可以通过魔术方法实现。例如:

  • 通过定义__eq__函数来改变默认的"等于"比较行为
  • 不等于运算符可以通过定义__ne__函数来自定义逻辑
  • 对于大于、小于等操作符需要定义__gt____lt__等方法
class MyDate:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __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 NotImplemented
        return not self.__eq__(other)

    def __lt__(self, other):
        if not isinstance(other, MyDate):
            return NotImplemented
        return (self.year, self.month, self.day) < (other.year, other.month, other.day)

    def __le__(self, other):
        if not isinstance(other, MyDate):
            return NotImplemented
        return (self.year, self.month, self.day) <= (other.year, other.month, other.day)

    def __gt__(self, other):
        if not isinstance(other, MyDate):
            return NotImplemented
        return (self.year, self.month, self.day) > (other.year, other.month, other.day)

    def __ge__(self, other):
        if not isinstance(other, MyDate):
            return NotImplemented
        return (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

注意:

  1. 通常只需定义__eq__,因为__ne__默认会取__eq__的反结果
  2. rich comparison操作符在没有自定义时会抛出错误
  3. 比较不同类对象时,会优先调用子类的方法

在实现rich comparison时,如果两个对象不是同一类,会如何处理?

当进行rich comparison时:

  • 如果 Y 是 X 的子类,优先使用 Y 的比较方法
  • 否则优先使用 X 的比较方法
    这意味着不同类对象的比较可能触发不同的比较逻辑
class Fruit:
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def __eq__(self, other):
        print("Fruit __eq__ called")
        if not isinstance(other, Fruit):
            return NotImplemented
        return self.name == other.name and self.weight == other.weight

    def __lt__(self, other):
        print("Fruit __lt__ called")
        if not isinstance(other, Fruit):
            return NotImplemented
        return self.weight < other.weight

class Apple(Fruit):
    def __init__(self, name, weight, color):
        super().__init__(name, weight)
        self.color = color

    def __eq__(self, other):
        print("Apple __eq__ called")
        if not isinstance(other, Apple):
            # 如果对方不是Apple,但可能是Fruit,可以委托给父类
            if isinstance(other, Fruit):
                return super().__eq__(other) # 或者自定义不同的逻辑
            return NotImplemented
        return super().__eq__(other) and self.color == other.color

    def __lt__(self, other):
        print("Apple __lt__ called")
        if not isinstance(other, Apple):
            if isinstance(other, Fruit): # 与Fruit比较权重
                return self.weight < other.weight
            return NotImplemented
        return 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: False

print(f"fruit1 == apple1: {fruit1 == apple1}") # Fruit __eq__ called (fruit1是Fruit类,优先调用其__eq__)
                                             # 输出: fruit1 == apple1: False

print(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 = x
        self.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 中,为什么两个相同的自定义对象在字典中会被视为不同的键?

原因:

  1. 自定义__eq__方法后,默认__hash__会被删除
  2. 需要同时自定义__hash__方法
  3. 必须保证相等对象具有相同hash
class Coordinate:
    def __init__(self, lat, lon):
        self.lat = lat
        self.lon = lon

    def __eq__(self, other):
        if not isinstance(other, Coordinate):
            return NotImplemented
        return 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}") # True

try:
    # 尝试将对象用作字典的键或放入集合
    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 = lat
        self.lon = lon

    def __eq__(self, other):
        if not isinstance(other, ProperCoordinate):
            return NotImplemented
        return self.lat == other.lat and self.lon == other.lon

    def __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函数?

要求:

  1. 必须返回整数
  2. 相等对象必须返回相同hash
  3. 推荐做法:
    def __hash__(self):
        return hash((self.attr1, self.attr2))
    
    避免直接返回常数,否则会导致大量哈希冲突
class Book:
    def __init__(self, title, author, isbn):
        self.title = title
        self.author = author
        self.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 1

book1 = 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_list

    def __hash__(self):
        # 注意:如果列表内容改变,哈希值也会改变
        # 这使得它不适合做字典的键
        # 为了能 hash,我们将列表转换为元组
        return hash(tuple(self.value_list))

    def __eq__(self, other):
        if not isinstance(other, MutableKey):
            return NotImplemented
        return self.value_list == other.value_list

    def __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 # 例如,只有激活且非空时为 True

    def __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:
    pass

shell = 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonetto1999

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值