在前面,我用了3篇文章解释python的面向对象:html
本篇是第4篇,用一个完整的示例来解释面向对象的一些细节。python
例子的模型是父类Employe和子类Manager,从类的定义开始,一步步完善直到类变得完整。网络
如今,假设Employe类有3个属性:名字name、职称job和月薪水pay。数据结构
定义这个类:app
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。ide
例如:函数
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()函数来处理。ui
例如:
>>> 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来调用父类方法的,虽然不适合后期维护。但好在并无任何影响。由于调用时明确指定了第一个参数为self,而self表明的是对象自身,因此逻辑上仍然是对本对象的属性self.pay进行修改。
Python支持另外一只更好的调用父类方法的方式:super()。这个函数有点复杂,但对于基本的调用父类方法来讲,用法无比的简单。修改上面的Manager类:
class Manager(Employe): def __init__(self, name, pay): super().__init__(name, "mgr", pay) def giveRaise(self, percent, bonus=0.10): super().giveRaise(percent + bonus)
测试下:
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序列化的文章。