基于股票大数据分析的Python入门实战(视频教学版)
上QQ阅读APP看书,第一时间看更新

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行的方式,即不建议通过对象来访问静态方法和类方法。

需要强调的是,静态方法和类方法会破坏类的封装性,那么无需实例化对象即可访问,所以使用时请慎重,确实有“无需实例化对象”的需求时,才能使用。