首页 新闻 会员 周边

编码细节。。。。。。

0
[已关闭问题] 关闭于 2026-07-23 14:54

在面向对象设计中,有一个重要的原则叫里氏替换原则(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, ...) “没有任何影响”。这在单继承下碰巧成立,但在多重继承中是致命的。
  • 改进方案:明确指出 Python 的 MRO(方法解析顺序) 机制。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 (对象不相等!)

在这个例子中,p1p2 的哈希值都是 1,但它们显然不是同一个人(p1 == p2 为 False)。这就是典型的哈希值相等,但对象不相等。

 

四、 核心解惑:发生冲突了,字典/集合怎么区分它们?

这才是理解 __hash____eq__ 必须绑定的终极原因!

当 Python 的字典(dict)或集合(set)在底层存储数据时,如果遇到哈希冲突(即两个不同的对象算出了相同的哈希值,被分到了同一个“桶”里),它会按照以下流程处理:

  1. 第一步:看哈希值(找桶) 字典通过 hash(obj) 算出哈希值,找到对应的“桶”(内存位置)。
  2. 第二步:发现桶里已经有东西了(发现冲突) 字典发现这个位置已经被占用了。
  3. 第三步:调用 __eq__ 进行“身份核实”(解决冲突) 字典会拿新来的对象,和桶里已有的对象进行 == 比较(底层调用 __eq__)。
    • 情况 A(__eq__ 返回 True):字典认为“哦,这是同一个对象,只是来更新数据的”,于是覆盖旧值。
    • 情况 B(__eq__ 返回 False):字典确认“这是哈希冲突!它们只是碰巧哈希值一样,其实是两个不同的对象”。于是,字典会在底层寻找下一个空闲的位置(这在底层算法中叫“开放寻址法”),把新对象存进去。

如果没有 __eq__ 会怎样?

如果你只写了 __hash__ 没写 __eq__,当发生哈希冲突时,字典就无法核实这两个对象到底是不是同一个。它可能会错误地把两个不同的对象当成同一个对象给覆盖掉,导致数据丢失!

  1. __hash__ 是“粗筛”:它负责快速把对象分配到一个“大类”或“桶”里。哈希值相等,只说明它们可能是同一个对象,或者它们碰巧被分到了同一个桶里(哈希冲突)。
  2. __eq__ 是“精查”:当“粗筛”发现冲突时,必须通过 __eq__ 进行精确比对。只有 __eq__ 返回 True,才代表对象真正相等。

这就是为什么我们常说:“对象相等(__eq__ 为 True),哈希值必须相等;但哈希值相等,对象不一定相等(可能是哈希冲突)。”在 Python 中,只有“不可变对象(Immutable)”才能被哈希,而元组(tuple)是 Python 内置的、最常用的、能容纳多个元素的不可变序列,

在 Python 中,一个对象要想能计算出哈希值(即能作为字典的 key 或放入 set 中),必须满足一个绝对前提:它的哈希值在生命周期内绝对不能改变。

  • 可变对象(如 list, dict, set):内容可以随时修改。如果允许它们计算哈希,一旦内容改变,哈希值就会变。这会导致它在字典中“丢失”(正如我们前面讨论过的)。因此,Python 直接禁止对它们求哈希。
  • 不可变对象(如 int, str, tuple, frozenset):创建后内容无法修改。因此它们的哈希值是稳定的,可以安全地被哈希。

当你有多个属性(比如 self.xself.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 相等且哈希相同,被去重了)

 

*Tesla*的主页 *Tesla* | 老鸟四级 | 园豆:2258
提问于:2026-07-23 14:49
< >
分享
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册