Python面向对象进阶:方法重写、运算符重载与序列化详解

顾星河
顾星河
发布时间:2026-10-13 | 3人浏览过
原创

本文深入解析Python面向对象编程的关键细节。通过Employe与Manager类的示例,演示如何在子类中合理重写父类方法以扩展功能。详细讲解__str__和__add__等魔术方法的运算符重载机制,以及通过序列化实现对象持久化存储的基本概念,帮助开发者构建更健壮、可复用的代码结构。

前言

在此前的系列文章中,我已经分三篇详细阐述了Python面向对象的基础知识:

  • 第一篇探讨了面向对象如何从代码复用的角度提升开发效率。
  • 第二篇讲解了如何为对象设置属性,确保数据封装的正确性。
  • 第三篇分析了类和对象名称空间的运作机制,厘清了变量查找顺序。

本篇作为第四篇,将通过一个完整的综合示例,深入剖析面向对象编程中的一些关键细节与最佳实践。

本示例构建了一个父类Employe和一个子类Manager,从最基础的类定义开始,逐步完善代码,直至形成一个功能完整的类结构。

定义Person类

假设Employe类包含三个核心属性:姓名name、职位job以及月薪pay。

以下是该类的定义代码:

class Employe():
 def __init__(self, name, job=None, pay=0):
 self.name = name
 self.job = job
 self.pay = pay

在__init__()构造函数中,为job参数设置了默认值None,表示员工当前未设定具体职位。对于无职位的员工,其薪资pay自然应为0。这种设计使得在创建Employe对象时,可以仅传入name参数,简化了实例化过程。

例如,我们可以这样创建对象:

if __name__ == "__main__":
 longshuai = Employe("Ma Longshuai")
 xiaofang = Employe("Gao Xiaofang", job="accountant", pay=15000)

上述代码中的if判断语句用于区分文件运行模式:当该py文件作为主程序直接执行时,if块内的代码会被运行;若作为模块被其他文件导入,则if块内的代码不会执行。这种模式在编写和测试模块代码时极为实用。

运行该py文件后,输出结果如下:

<__main__.Employe object at 0x01321690> <__main__.Employe object at 0x01321610>

添加方法

每个Employe对象的name属性由姓和名组成,中间以空格分隔。现在我们需要从姓名中提取出名字部分。对于标准的“姓 名”字符串格式,可以利用字符串的split()函数进行处理。

例如:

>>> name = "Ma Longshuai"
>>> name.split()[-1]
'Longshuai'

于是,我们可以在longshuai和xiaofang这两个Employe对象上执行以下操作:

print(longshuai.name.split()[-1])
print(xiaofang.name.split()[-1])

运行结果如下:

Longshuai Xiaofang

类似地,若需为员工增加10%的薪水,可以在每个Employe对象上执行以下操作:

xiaofang.pay *= 1.1
print(xiaofang.pay)

无论是提取名字还是加薪操作,这些功能都是Employe类共用的,每个员工实例均可使用。因此,更合理的做法是将它们定义为类的方法,以便后续代码复用:

class Employe():
 def __init__(self, name, job=None, pay=0):
 self.name = name
 self.job = job
 self.pay = pay

 def lastName(self):
 return self.name.split()[-1]

 def giveRaise(self, percent):
 self.pay = int(self.pay * (1 + percent))

if __name__ == "__main__":
 longshuai = Employe("Ma Longshuai")
 xiaofang = Employe("Gao Xiaofang", job="accountant", pay=15000)
 
 print(longshuai.lastName())
 print(xiaofang.lastName())
 xiaofang.giveRaise(0.10)
 print(xiaofang.pay)

在上面的giveRaise()方法中,使用了int()进行类型转换。这是因为整数乘以浮点数(如15000 * 0.1)会返回浮点数(例如1500.0)。为了保持薪资为整数格式,我们使用int()将其转换为整数。

定义子类并重写父类方法

接下来,我们定义Employe的子类Manager。

class Manager(Employe):

Manager的薪水计算逻辑是在原有薪水基础上增加一笔奖金。因此,需要重写父类的giveRaise()方法。重写父类方法主要有两种方式:

  • 完全覆盖父类方法
  • 在父类方法的基础上进行扩展

虽然第一种方式(完全覆盖)通过拷贝修改父类代码非常便捷,但它并不合理。更合理的方式是采用第二种(基于扩展)。

以下是第一种方式的实现代码:

class Manager(Employe):
 def giveRaise(self, percent, bonus=0.10):
  self.pay = int(self.pay * (1 + percent + bonus)

这种重写方式逻辑简单,但完全否定了父类的giveRaise()方法,重新定义了一套完整的逻辑。这种方式存在缺陷:如果未来修改了Employe中的giveRaise()计算逻辑,Manager中的对应方法也必须同步修改,增加了维护成本。

以下是第二种基于父类方法扩展的实现,这是推荐的重写方式。

class Manager(Employe):
 def giveRaise(self, percent, bonus=0.10):
  Employe.giveRaise(self, percent + bonus)

第二种方式是在子类的giveRaise()方法内部调用父类的giveRaise()方法。其优势在于:当需要修改薪水计算逻辑时,只需修改Employe或Manager中的一方,无需同时修改多处代码,提高了代码的可维护性。

另外需要注意的是,上述代码通过硬编码类名Employe来调用父类方法。在Python中,目前尚无其他更简洁的语法糖,只能通过这种硬编码方式实现。但这并无实际负面影响,因为调用时明确指定了第一个参数为self,self代表当前对象实例,因此逻辑上仍然是修改当前对象的属性self.pay。

让我们进行测试:

if __name__ == "__main__":
 wugui = Manager("Wu Xiaogui", "mgr", 15000)
 wugui.giveRaise(0.1, 0.1)
 print(wugui.pay)

在重写方法时,只要条件允许,应优先选择在父类基础上进行扩展重写。如果确实需要定义完全不同的方法,可以不重写,而是在子类中定义新方法。当然,若确有需求必须重写且需否定父类方法,则只能如此,但这通常暗示类的设计存在不合理之处。

定制子类构造方法

对于子类Manager,每次创建对象时其实无需手动传递参数"job=mgr",因为这是该子类固有的属性。因此,在构造Manager对象时,可以自动设置"job=mgr"。

为此,需要在Manager类中重写__init__()方法。既然涉及重写,同样存在两种方式:(1)完全覆盖父类方法,(2)在父类方法上扩展。无论何种场景,始终应选择第二种方式。

以下是Manager类的完整定义:

class Manager(Employe):
 def __init__(self, name, pay):
  Employe.__init__(self, name, "mgr", pay)

 def giveRaise(self, percent, bonus=0.10):
  Employe.giveRaise(self, percent + bonus)

现在,构造Manager对象时,只需传入name和pay参数即可:

if __name__ == "__main__":
 wugui = Manager("Wu Xiaogui", 15000)
 wugui.giveRaise(0.1, 0.1)
 print(wugui.pay)

子类必须重写方法

某些父类中的方法可能强制要求子类必须重写。

本文的示例难以直观解释这一点。下面简单使用父类Animal、子类Horse、Sheep和Cow来说明,该例子源自我撰写的面向对象系列第一篇文章:从代码复用开始。

现在为动物定义叫声speak()方法,其作用是输出“谁发出了什么声音”。代码如下:

class Animal:
 def __init__(self, name):
  self.name = name
 def speak(self):
  print(self.name + " speak " + self.sound())
 def sound(self):
  raise NotImplementedError("you must override this method")

在这段代码中,speak()方法调用了sound()方法,但Animal类中的sound()方法明确抛出异常“你必须自己实现这个方法”。

原因在于:每种动物发出的叫声不同,而此处通过方法而非属性来返回叫声,因此每个子类必须定义自己的叫声。如果子类不定义sound(),子类对象调用self.sound()时会在父类Animal的名称空间中搜索,而父类的sound()会抛出错误。

现在在子类中重写sound(),但Cow类不重写。

class Horse(Animal):
 def sound(self):
  return "neigh"

class Sheep(Animal):
 def sound(self):
  return "baaaah"

class Cow(Animal):
 pass

进行测试:

h = Horse("horseA")
h.speak()

s = Sheep("sheepA")
s.speak()

c = Cow("cowA")
c.speak()

结果符合预期:h.speak()和s.speak()均正常输出,但c.speak()会抛出“you must override this method”的异常。

进一步思考,如果父类中不定义sound()会如何?同样会在c.speak()时抛出错误。虽然都会导致程序终止,但这已违背面向对象代码复用的原则:对于对象公有的属性,应抽取到类中;对于类公有的属性,应抽取到父类中。sound()显然是每种动物都应具备的属性,要么定义为子类变量,要么通过类方法返回。

前文曾提及,如果可以,应尽量避免定义类变量,因为这破坏了面向对象的封装原则,打开了“黑匣子”。因此,最合理的方法是每个子类重写父类的sound(),且父类中的sound()强制要求子类重写。

运算符重载

如果使用print()输出我们自定义类的对象(如Employe对象),得到的将是元数据信息,包括类型和内存地址。

例如:

print(longshuai)
print(xiaofang)

## 结果:
<__main__.Employe object at 0x01321690>
<__main__.Employe object at 0x01321610>

我们可以自定义print()如何输出对象,只需定义类的__str__()方法即可。只要在类中定义了该方法,print()输出对象时就会自动调用__str__()获取返回值,并将该值输出。

例如,在输出每个Employe对象时,都输出它的name、job、pay,并以自定义格式展示。

class Employe():
 def __init__(self, name, job=None, pay=0):
  self.name = name
  self.job = job
  self.pay = pay

 def lastName(self):
  return self.name.split()[-1]

 def giveRaise(self, percent):
  self.pay = int(self.pay * (1 + percent))

 ## 重载__str__()方法
 def __str__(self):
  return "[Employe: %s, %s, %s]" % (self.name, self.job, self.pay)

现在再使用print()输出对象,将得到对象的具体信息,而非元数据:

print(longshuai)
print(xiaofang)

## 结果:
[Employe: Ma Longshuai, None, 0]
[Employe: Gao Xiaofang, accountant, 15000]

实际上,print()总是会调用对象的__str__()。如果类中没有定义__str__(),就会查找父类中的__str__()。这里Employe的父类是祖先类object,它恰好有一个__str__():

>>> object.__dict__["__str__"]
<slot wrapper '__str__' of 'object' objects>

换句话说,当Employe中定义了__str__(),即意味着重载了父类object的__str__()方法。而该方法正好被print()调用,因此将这种行为称为“运算符重载”。

可能从print()上难以直观感受为何称为运算符,换一个例子便很好理解。__add__()决定了加号+的运算模式。例如3 + 2之所以等于5,是因为int类中定义了__add__()。

>>> a=3
>>> type(a)
<class 'int'>

>>> int.__dict__["__add__"]
<slot wrapper '__add__' of 'int' objects>

这使得每次执行数值加法运算时,都会调用__add__()来决定如何相加:

实际上,在类中定义构造函数__init__()也是一种运算符重载,它在每次创建对象时被调用。

还有许多运算符可以重载,包括加减乘除、字符串串联、大小比较等,无论是否与运算符直接相关,均可被重载。后续将专门用一篇文章介绍运算符重载。

序列化

对象本质上是一种数据结构,数据结构可以进行序列化。通过将对象序列化,可以实现对象的本地持久化存储,还可以通过网络套接字发送给网络对端,然后通过反序列化还原得到完全相同的原始数据。

序列化并非本文重点,此处仅作简要介绍。后续我将撰写几篇专门介绍Python序列化的文章。

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表