在前面, 我用了 3 篇文章解释 python 的面向对象:
面向对象: 从代码复用开始
面向对象: 设置对象属性
类和对象的名称空间
本篇是第 4 篇, 用一个完整的示例来解释面向对象的一些细节.
例子的模型是父类 Employe 和子类 Manager, 从类的定义开始, 一步步完善直到类变得完整.
定义 Person 类
现在, 假设 Employe 类有 3 个属性: 名字 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)
无论是截取 name 的名部分, 还是加薪水的操作, 都是 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 中的 giveRaise()方法也要修改.
下面是第二种在父类方法基础上扩展, 这是合理的重写方式.
- 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 序列化的文章.
来源: https://www.cnblogs.com/f-ck-need-u/p/10099735.html