3.1 把属性和方法封装成类,方便重复使用
假如我们要经常实现买卖股票的功能,一种做法是在每一处都定义一遍股票价格交易日期等属性,外带实现买卖功能的方法。与这种极不方便做法相比,基于面向对象的做法是,用类(Class)把股票的相关属性和方法封装起来,用的时候再通过创建实例来操作具体的股票。注意:在面向对象的程序设计中,更习惯把传统函数(Function)功能模块称为方法(Method),因为方法是面向对象程序设计的标准术语。因此,在本书描述中当只强调功能时,会混用“函数”和“方法”不太加以区分。而在特别强调面向对象程序设计概念的描述中,则一般只使用“方法”。
上述描述中涉及两个概念:类和对象。其中类是比较抽象的概念,比如人类,股票类,而对象也叫类的实例,是相对具体的概念,比如人类的实例是“张三”这个活生生的人,股票类的实例则是某一只具体的股票。对象是通过类来创建的,创建对象的过程也叫“实例化”。
3.1.1 在Python中定义和使用类
当大家熟悉面向对象程序设计的思想后,一提到封装,就应当想到类,因为在项目中是在类里封装属性和方法。
在下面的ClassUsageDemo.py范例程序中,可以看到定义和使用类的基本方式。通过这段代码,可以看到类是通过封装属性(比如stockCode和price)和方法实现了功能的重复使用(简称复用)。
1 # !/usr/bin/env python 2 # coding=utf-8 3 # 定义类 4 class Stock: 5 def __init__(self, stockCode, price): 6 self.stockCode, self.price=stockCode,price 7 def get_stockCode(self): 8 return self.stockCode 9 def set_stockCode(self,stockCode): 10 self.stockCode=stockCode 11 def get_price(self): 12 return self.price 13 def set_price(self,price): 14 self.price=price 15 def display(self): 16 print("Stock code is:{}, price is:{}.".format(self.stockCode,self.price)) 17 # 使用类 18 myStock=Stock("600018",50) # 实例化一个对象myStock 19 myStock.display() #Stock code is:600018, price is:50. 20 # 更改其中的值 21 myStock.set_stockCode("600020") 22 print(myStock.get_stockCode()) # 600020 23 myStock.set_price(60) 24 print(myStock.get_price()) # 60
在第4行中通过class关键字定义了一个名为Stock的类,在之后的第5行到第16行中通过def定义了Stock类的若干个方法,请注意,在这些方法中都能看到self关键字。
self是指自身类,这里即是指Stock类,比如在第6行的init方法中,是用self.stockCode, self.price=stockCode,price的方式,用参数传入的stockCode和price给类的对应属性赋值。在第7行到第14行的get和set类方法中,用参数给self指向的本类的对应属性赋值。而在第16行的display方法中,通过print语句输出了本类中的两个属性的值。
在第18行中定义了Stock类的一个实例对象myStock,并在实例化时,传入了该对象对应的两个属性的值。在第19行中,通过myStock这个实例(请注意是myStock实例,而不是Stock类)调用了display方法。
请注意,调用方法(或函数)的主体是实例,而不是类(Stock.display),这是符合逻辑的。比如在展示股票信息时,不是展示抽象的股票类信息(即Stock类的信息),而是要展示具体股票实例对象(比如600018)的信息。
在第21行和第23行中通过set方法变更了属性值,在之后的第22行和第24行的print语句中,是通过调用get方法得到了myStock实例中的值。
3.1.2 通过__init__了解常用的魔术方法
在刚才的范例程序中看到了一个现象,在通过myStock=Stock("600018", 50)实例化一个股票对象时,会自动触发Stock类里的__init__方法。像这样在开头和结尾都是两个下画线的方法叫魔术方法,它们会在特定的时间点被自动触发,比如__init__方法会在初始化类的时候被触发。
魔术方法虽然不少,但在实际项目中经常被用到的却不多。下面通过MagicFuncDemo.py范例程序来看看使用频率比较高的魔术方法。
1 # !/usr/bin/env python 2 # coding=utf-8 3 # 定义类 4 class Stock: 5 def __init__(self,stockCode): 6 print("in __init__") 7 self.stockCode=stockCode 8 # 回收类的时候被触发 9 def __del__(self): 10 print("In __del__") 11 def __str__(self): 12 print("in __str__") 13 return "stockCode is: "+self.stockCode 14 def __repr__(self): 15 return "stockCode is: "+self.stockCode 16 def __setattr__(self, name, value): 17 print("in __setattr__") 18 self.__dict__[name]=value # 给类中的属性名分配值 19 def __getattr__(self, key): 20 print("in __setattr__") 21 if key == "stockCode": 22 return self.stockCode 23 else: 24 print("Class has no attribute '%s'" % key) 25 # 初始化类,并调用类里的方法 26 myStock=Stock("600128") # 触发__init__和 __setattr__方法 27 print(myStock) # 触发__str__和__repr__方法 28 myStock.stockCode="600020" # 触发__setattr__方法
在第5行定义的__init__方法内,在第26行创建对象实例时会被触发,这里的__init__方法只有两个参数,而前一节的范例程序中有3个。事实上,该方法可以支持多个或多种不同参数的组合,在后文提到“重载”(Overloaded)概念时会详细介绍。
第9行的__del__方法会在类被回收时触发,它有些像析构函数,可以在范例程序中使用打印语句来查看类的回收时间点,如果在类里还打开了文件等的资源,也可以在这个方法中关闭这些资源,以交还给系统。
第11行的__str__和第14行的__repr__方法一般会配套使用,这两个方法是在第27行被触发。当调用print方法打印对象时,首先会触发__repr__方法,这里如果不写__str__方法,运行时会报错,原因是在print方法的参数里传入的是myStock对象,而打印时,一般是会输出字符串,所以这里就需要通过__str__方法定义“把类转换成字符串打印”的方法,在第13行中打印stockCode的信息。
相比之下,第16行的__setattr__和第19行的__getattr__被调用的频率就没有之前的方法高,它们分别会在设置和获取属性时被触发。它们被调用的频率不高的原因是,一般在代码中是通过诸如get_price和set_price的方式获取和设置指定的属性值,而在设置和获取属性值时,一般无需执行其他的操作,所以就无需在__setattr__和__getattr__这两个魔术方法里编写“自动触发”的操作。出于同样的原因,__setitem__和__getitem__这两个魔术方法被调用的频率也不高。
3.1.3 对外屏蔽类中的不可见方法
出于封装性的考虑,类的一些方法就不该让外部使用,比如启动汽车,就应该使用提供的“用钥匙发动”的方法启动汽车,而不该通过汽车类里的“连接线路启动”的方法来启动。
出于同样的道理,为了防止误用,在定义类时,应当通过控制访问权限的方式来限制某些方法和属性被外部调用。
在Python语言中,诸如_xx这样以单下画线开头的是protected(受保护)类型的变量和方法,它们只能在本类和子类中访问。而诸如__xx以双下画线表示的是private(私有)类型的变量和方法,它们只能在本类中被调用。
下面通过ClassAvailableDemo.py范例程序来演示私有变量和方法的使用或调用方式,而protected类型的变量和方法,将在介绍“继承”章节中说明。
1 # !/usr/bin/env python 2 # coding=utf-8 3 # 定义类 4 class Car: 5 def __init__(self,owner,area): 6 self.owner=owner 7 self.__area=area 8 def __engineStart(self): 9 print("Engine Start") 10 def start(self): 11 print("Start Car") 12 self.__engineStart() 13 def get_area(self): 14 return self.__area 15 def set_area(self,area): 16 self.__area=area 17 # 使用变量 18 carForPeter=Car("Peter",'ShangHai') 19 # print(carForPeter.__area) 20 print(carForPeter.owner) # Peter 21 carForPeter.set_area("HangZhou") 22 print(carForPeter.get_area()) # HangZhou 23 carForPeter.start() 24 # carForPeter.__engineStart() # 报错
在第7行的__init__方法中通过输入参数给self.__area变量赋值,这里的__area是私有变量,而第8行的__engineStart是私有方法。
这些私有变量只能在Car类内部被用到,如果去除第19行的注释,程序运行就会出错,因为企图在类的外部使用私有变量。与之相比,由于owner是公有变量,因此通过第20行的代码直接在类的外部通过类的实例来访问。同样,如果去掉第24行的注释,也会报错,因为企图通过实例调用私有的方法。
下面给出在项目中使用私有变量和私有方法的一些调用准则。
(1)一定要把不该让外部看到的属性和方法设置成私有的(或受保护的),比如上述范例程序第8行的__engineStart属于汽车启动时的内部操作,不该让用户直接调用,所以应该毫不犹豫地设置成私有。
(2)私有的或受保护的属性,应该通过如第13行和第15行的get类和set类的方法供外部调用。
(3)应该尽可能地缩小类和属性的可见范围。比如把某个私有方法设置成公有的,这在语法上不会有错,而且用起来会更方便,因为能在类外部直接调用了。但是,一旦让外部用户能直接调用内部方法,就相当于破坏了类的封装特性,很容易导致程序出错,所以上述“访问私有变量和私有方法而报错”的特性,其实是一种保护机制。
(4)如果没有特殊理由,一般都是把属性设置成私有的或受保护的,同时提供公有的get和set类方法供外部访问,而不该直接把属性设置成公有的。
3.1.4 私有属性的错误用法
可以这样说,初学者在使用私有变量时,很容易出现如下PrivateBadUsage.py范例程序中所示的问题。
1 # !/usr/bin/env python 2 # coding=utf-8 3 # 定义类 4 class Car: 5 def __init__(self,area): 6 self.__area=area 7 def get_area(self): 8 return self.__area 9 def set_area(self,area): 10 self.__area=area 11 # 使用类 12 carForPeter=Car("ShangHai") 13 carForPeter.__area="HangZhou" 14 print(carForPeter.get_area()) # 发现并没改变__area 15 carForPeter.set_area("WuXi") 16 print(carForPeter.get_area()) # WuXi 17 carForPeter._Car__area="Bad Usage" # 不建议这样做 18 print(carForPeter.get_area()) # 发现修改了__area的值
在这个范例程序的第6行中,在__init__的初始化方法内,给__area这个私有变量赋值,同时在第7行和第9行中,定义了针对该私有属性的get和set方法。
在第13行中,看上去是直接通过carForPeter对象给__area私有变量赋值,但这里有两点出乎我们的意料:第一,明明不能在外部直接访问私有变量,为什么这行代码运行时没报错呢?第二,通过第14行的代码打印carForPeter.get_area()的值,发现carForPeter内部的__area变量依然是ShangHai,没有变成HangZhou。
原因很简单,在实例化对象的时候,Python会把类的私有变量改个名字,该名字的规则如第17行所示,是_类名加上私有变量名。也就是说,前面定义的私有变量被转换成_Car__area,而在第13行中,是在carForPeter这个对象里新建了一个属性__area,并给它赋了HangZhou这个值,因此在第13行中没有对Car 类的私有变量__area进行修改。
在第17行中,进一步验证了“对私有变量进行改名”的这个规则,这里给_Car__area变量赋予了一个新的值,在项目中不建议这样做,应该通过对应的get和set方法操作私有属性。第18行中的输出结果是Bad Usage,由此验证了_Car__area变量确实对应到Car内部私有的__area,也就是说验证了Python对私有变量的“更名规则”。
最后要强调的是,本节讲述了私有变量的更名规则,目的不是让大家通过变更后的名字来访问私有变量,而是让大家了解这个技术细节,从而避免上述似是而非的使用私有属性的不规范和不建议的用法。
3.1.5 静态方法和类方法
前文介绍了通过对象.方法()的形式来调用方法,比如张三.吃饭(),而不是人类.吃饭(),因为吃饭的主体是具体的某个人,而不是抽象的人类概念。但是,在一些应用场景里,无需实例化对象就可以调用方法。
比如在提供计算功能的工具类里,类本身即可当成“计算工具”,再实例化对象就没意义了,对于这种情况就可以通过定义静态方法和类方法来简化调用过程。
在Python语言中,类方法(classmethod)和静态方法(staticmethod)的差别是,类方法的第一个参数必须是指向本身的引用,而静态方法可以没有任何参数。在下面的MethodDemo.py范例程序中来看一下两者的常见用法。
1 # !/usr/bin/env python 2 # coding=utf-8 3 class CalculateTool: 4 __PI=3.14 5 @staticmethod 6 def add(x,y): 7 __result=x+y 8 print(x + y) 9 @classmethod 10 def calCircle(self,r): 11 print(self.__PI*r*r) 12 CalculateTool.add(23, 22) # 输出45 13 CalculateTool.calCircle(1) # 输出3.14 14 # 不建议通过对象访问静态方法和类方法 15 tool=CalculateTool() 16 tool.add(23, 22) 17 tool.calCircle(1)
在第6行的add方法前面加了@staticmethod注解,用来说明这个方法是静态方法,而给第10行的calCircle方法在第9行加了@classmethod注解,说明这个方法是类方法。
在add这个静态方法中,由于没有通过self之类的参数来指向本身,因此它不能访问类的内部属性和方法,而对于calCircle这个类方法而言,由于第一个参数self指向类本身,因此能访问类的变量PI。在第12行和第13行中,通过类名CalculateTool来直接访问静态方法和类方法,这里不建议使用第15行到第17行的方式,即不建议通过对象来访问静态方法和类方法。
需要强调的是,静态方法和类方法会破坏类的封装性,那么无需实例化对象即可访问,所以使用时请慎重,确实有“无需实例化对象”的需求时,才能使用。