在面向对象设计中,有一个重要的原则叫里氏替换原则(Liskov Substitution Principle)。它要求子类对象必须能够替换掉所有父类对象,而程序逻辑不会出错
# 假设父类方法是:
# def giveRaise(self, percent):
# 子类重写为:
def giveRaise(self, percent, bonus=0.10):
super().giveRaise(percent + bonus)
在重写 giveRaise 方法时,作者改变了方法的签名
技术误导:对“硬编码调用父类”的危害轻描淡写
在单继承的情况下,硬编码 Employe.giveRaise(self, ...) 确实没有运行时的影响。
但在多重继承(尤其是菱形继承)的情况下,硬编码类名会直接破坏 Python 的 MRO(方法解析顺序),导致父类方法被重复调用多次(即经典的 Diamond Problem)。
作为技术教程,应该明确指出硬编码在多重继承下的致命缺陷,从而顺理成章地引出为什么必须使用 super(),而不是告诉读者“没有影响”。
父类 Employee
class Employee: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay def last_name(self): return self.name.split()[-1] def give_raise(self, percent): # 改进:使用 round() 进行四舍五入,避免 int() 直接截断导致财务精度丢失 self.pay = round(self.pay * (1 + percent)) def __str__(self): # 改进:使用现代 Python 的 f-string 进行字符串格式化 return f"[Employee: {self.name}, {self.job}, {self.pay}]"
子类 Manager
我们将 bonus 从方法参数移到构造函数中,以遵循里氏替换原则(LSP);并统一使用 super(),彻底摒弃硬编码调用
class Manager(Employee): # 改进:在初始化时接收 bonus,并设置默认值 def __init__(self, name, pay, bonus=0.10): # 改进:统一使用 super(),避免硬编码类名,防止多重继承下的 MRO 混乱 super().__init__(name, job="mgr", pay=pay) # 改进:将 bonus 作为实例属性,而不是 give_raise 方法的参数 self.bonus = bonus def give_raise(self, percent): # 改进:保持方法签名与父类完全一致 (只有 self 和 percent) # 这样外部调用时,多态行为才是可预期的 super().give_raise(percent + self.bonus)
测试代码,展示面向对象中“多态”
if __name__ == "__main__": # 1. 正确实例化对象(修复了原先参数数量不匹配的 TypeError) emp1 = Employee("John Doe", "developer", 10000) # Manager 默认 bonus 是 0.10,这里我们显式指定为 0.15 mgr1 = Manager("Wu Xiaogui", 15000, bonus=0.15) # 2. 展示多态(Polymorphism)的正确用法 # 将不同类型的员工放入同一个列表中 staff = [emp1, mgr1] print("--- 涨薪前 ---") for person in staff: print(person) # 统一调用 give_raise(0.10),无需关心对象的具体类型 # 普通员工涨 10%,Manager 涨 10% + 15% = 25% for person in staff: person.give_raise(0.10) print("\n--- 涨薪后 ---") for person in staff: print(person)
1. 为什么要将 bonus 移到 __init__ 中?(里氏替换原则)
give_raise(self, percent, bonus=0.10) 中增加了 bonus 参数。这破坏了里氏替换原则(Liskov Substitution Principle)。如果外部代码遍历一个包含普通员工和经理的列表,统一调用 obj.give_raise(0.1),对于经理来说,由于默认参数的存在,实际加薪变成了 20%,这会导致业务逻辑失控。give_raise(self, percent) 与父类完全一致。将 bonus 作为经理特有的状态(属性) 在初始化时绑定到对象上。这样,多态调用时行为才是安全且符合预期的。2. 为什么必须使用 super() 而不能硬编码父类名?
Employee.give_raise(self, ...) “没有任何影响”。这在单继承下碰巧成立,但在多重继承中是致命的。super() 并不是简单地“调用父类”,而是“调用 MRO 链条中的下一个类”。使用 super() 可以确保在复杂的菱形继承结构中,每个父类的方法只被调用一次。硬编码类名会直接破坏 MRO,导致方法被重复执行或调用错误的类。因此,在 Python 3 中,应永远优先使用 super()。
在 Python 中,有一条铁律:
如果两个对象相等(
a == b为 True),那么它们的哈希值必须相等(hash(a) == hash(b))。 (反之不成立:哈希值相等,对象不一定相等,这叫哈希冲突:“哈希冲突”,我们先用一句话来概括: 两个内容完全不同的对象,经过哈希函数计算后,得到了相同的哈希值(整数))基于这条铁律,Python 3 引入了一个自动保护机制: 如果你在一个类中重写了
__eq__,但没有 显式重写__hash__,Python 会自动将该类的__hash__设置为None。这意味着,一旦你重写了
__eq__,你的对象就会自动变成“不可哈希(unhashable)”,无法再放入set或作为dict的 key。
代码演示:在 Python 中人为制造哈希冲突
我们可以通过重写 __hash__ 方法,故意让不同的对象返回相同的哈希值,来看看会发生什么:
class Person: def __init__(self, name, age): self.name = name self.age = age # 故意让所有人的哈希值都是 1(制造极端的哈希冲突) def __hash__(self): return 1 # 只有名字和年龄都一样,才认为是同一个人 def __eq__(self, other): if not isinstance(other, Person): return NotImplemented return self.name == other.name and self.age == other.age # 创建两个完全不同的人 p1 = Person("张三", 20) p2 = Person("李四", 30) print(f"p1 的哈希值: {hash(p1)}") # 输出: 1 print(f"p2 的哈希值: {hash(p2)}") # 输出: 1 print(f"哈希值相等吗? {hash(p1) == hash(p2)}") # 输出: True (哈希冲突!) print(f"p1 和 p2 是同一个人吗? {p1 == p2}") # 输出: False (对象不相等!)
在这个例子中,p1 和 p2 的哈希值都是 1,但它们显然不是同一个人(p1 == p2 为 False)。这就是典型的哈希值相等,但对象不相等。
四、 核心解惑:发生冲突了,字典/集合怎么区分它们?
这才是理解 __hash__ 和 __eq__ 必须绑定的终极原因!
当 Python 的字典(dict)或集合(set)在底层存储数据时,如果遇到哈希冲突(即两个不同的对象算出了相同的哈希值,被分到了同一个“桶”里),它会按照以下流程处理:
hash(obj) 算出哈希值,找到对应的“桶”(内存位置)。__eq__ 进行“身份核实”(解决冲突) 字典会拿新来的对象,和桶里已有的对象进行 == 比较(底层调用 __eq__)。
__eq__ 返回 True):字典认为“哦,这是同一个对象,只是来更新数据的”,于是覆盖旧值。__eq__ 返回 False):字典确认“这是哈希冲突!它们只是碰巧哈希值一样,其实是两个不同的对象”。于是,字典会在底层寻找下一个空闲的位置(这在底层算法中叫“开放寻址法”),把新对象存进去。如果没有 __eq__ 会怎样?
如果你只写了 __hash__ 没写 __eq__,当发生哈希冲突时,字典就无法核实这两个对象到底是不是同一个。它可能会错误地把两个不同的对象当成同一个对象给覆盖掉,导致数据丢失!
__hash__ 是“粗筛”:它负责快速把对象分配到一个“大类”或“桶”里。哈希值相等,只说明它们可能是同一个对象,或者它们碰巧被分到了同一个桶里(哈希冲突)。__eq__ 是“精查”:当“粗筛”发现冲突时,必须通过 __eq__ 进行精确比对。只有 __eq__ 返回 True,才代表对象真正相等。这就是为什么我们常说:“对象相等(__eq__ 为 True),哈希值必须相等;但哈希值相等,对象不一定相等(可能是哈希冲突)。”在 Python 中,只有“不可变对象(Immutable)”才能被哈希,而元组(tuple)是 Python 内置的、最常用的、能容纳多个元素的不可变序列,
在 Python 中,一个对象要想能计算出哈希值(即能作为字典的 key 或放入 set 中),必须满足一个绝对前提:它的哈希值在生命周期内绝对不能改变。
当你有多个属性(比如 self.x 和 self.y)需要组合起来计算哈希时,你必须把它们放进一个“容器”里。这个容器本身必须是不可变的,元组(tuple)正是完美符合这一要求的内置容器。
x, y = 1, 2
# ✅ 成功:元组是不可变的,可哈希
print(hash((x, y)))
# ❌ 报错:列表是可变的,不可哈希
print(hash([x, y])) # TypeError: unhashable type: 'list'
# ❌ 报错:字典是可变的,不可哈希
print(hash({'x': x, 'y': y})) # TypeError: unhashable type: 'dict'
编写 __hash__ 的最佳实践:将参与 __eq__ 比较的属性打包成一个元组(tuple),然后对元组求哈希。
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): # 规范写法:类型不匹配时返回 NotImplemented,而不是 False if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): # 将参与比较的属性组成元组,利用元组自带的哈希机制 return hash((self.x, self.y)) p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # True print(hash(p1) == hash(p2)) # True # 现在可以完美放入集合和字典中 my_set = {p1, p2} print(len(my_set)) # 输出: 1 (因为 p1 和 p2 相等且哈希相同,被去重了)