赵走x博客
网站访问量:151569
首页
书籍
软件
工具
古诗词
搜索
登录
Python与数据挖掘:16、Bokeh
Python与数据挖掘:15、Matplotlib
Python与数据挖掘:14、scikit-learn
Python与数据挖掘:13、Scipy
Python与数据挖掘:12、Pandas
Python与数据挖掘:11、numpy
Python与数据挖掘:10、Python实用模块介绍
Python与数据挖掘:9、面向对象编程
Python与数据挖掘:8、函数
Python与数据挖掘:7、Python基础入门---文件的读写
Python与数据挖掘:6、Python基础入门---数据结构
Python与数据挖掘:5、Python基础入门---流程控制
Python与数据挖掘:4、Python基础入门---数字数据
Python与数据挖掘:3、Python基础入门---常用操作符
Python与数据挖掘:2、Python开发环境的搭建
Python与数据挖掘:1、数据挖掘概述
Python与数据挖掘:9、面向对象编程
资源编号:75853
书籍
Python 与数据挖掘
热度:85
在前面讲解了Python的主要内建对象类型(数字,列表,元组,字典,字符串),本章我们将介绍如何自定义对象。Python是一门面向对象编程 的语言,因此自定义对象是Python语言的一个核心。本章将先从面向对象的思想开始,然后逐步介绍Python的类和对象。类使得程序设计更加抽象,通过类的继承 (Inheritance)和组合 (Composition)使得程序语言更接近人类的语言。
在前面讲解了Python的主要内建对象类型(数字,列表,元组,字典,字符串),本章我们将介绍如何自定义对象。Python是一门面向对象编程 的语言,因此自定义对象是Python语言的一个核心。本章将先从面向对象的思想开始,然后逐步介绍Python的类和对象。类使得程序设计更加抽象,通过类的继承 (Inheritance)和组合 (Composition)使得程序语言更接近人类的语言。 # 1、简介 ### 1.简单的例子 面向对象出现以前,结构化程序设计是程序设计的主流,结构化程序设计又称为面向过程的程序设计。面向过程是分析出解决问题所需要的步骤,然后用函数一步一步实现这些步骤,使用的时候一个一个依次调用就可以了。而面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。例如五子棋,面向过程的设计思路就是首先分析问题的步骤:①开始游戏,②黑子先走,③绘制画面,④判断输赢,⑤轮到白子,⑥绘制画面,⑦判断输赢,⑧返回步骤2,⑨输出最后结果。把上面每个步骤分别用函数来实现,问题就解决了。而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为:①黑白双方,这两方的行为是一模一样的;②棋盘系统,负责绘制画面;③规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的输入就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,在面向过程的设计中,需要多个步骤执行该任务。但这样很可能导致不同步骤的绘制棋局程序不同,因为设计人员会根据实际情况对绘制棋局的程序进行简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。 ### 2.面向对象的优点 在面向过程程序设计中,问题被看作一系列需要完成的任务,解决问题的焦点集中于函数。其中函数是面向过程的,即它关注如何根据规定的条件完成指定的任务。在多函数程序中,许多重要的数据被放置在全局数据区,这样它们可以被所有的函数访问。每个函数都可以具有它们自己的局部数据。这种结构很容易造成全局数据在无意中被其他函数改动,因而程序的正确性不易保证。面向对象程序设计的出发点之一就是弥补面向过程程序设计中的一些缺点:对象是程序的基本元素,它将数据和操作紧密地连接在一起,并保护数据不会被外界的函数意外地改变。因此面向对象有如下优点: * 1)数据抽象的概念可以在保持外部接口不变的情况下改变内部实现,从而减少甚至避免对外界的干扰。 * 2)通过继承可以大幅减少冗余的代码,并可以方便地扩展现有代码,提高编码效率,也降低了出错概率,降低了软件维护的难度。 * 3)结合面向对象分析、面向对象设计,允许将问题域中的对象直接映射到程序中,减少软件开发过程中中间环节的转换过程。 ### 3.何时使用面向对象编程 面向对象的程序与人类对事物的抽象理解密切相关。举一个例子,虽然我们不知道精灵宝可梦这款游戏(又名口袋妖怪)的具体源码,但可以确定的是,它的程序是通过面向对象的思想编写的。我们将游戏中的每种精灵看作一个类,而具体的某只精灵就是其中一个类的一个实例对象,所以每种精灵的程序具有一定的独立性。程序员可以同时编写多只精灵的程序,它们之间不会相互影响。为什么这里我们不能使用面向过程编程呢?大家试想一下,如果程序员要开发新的精灵,那么就必须对之前的程序做大规模的修改,以使程序的各个函数能够正常工作(以前的函数没有新精灵的数据)。现在的程序和软件开发都是使用面向对象编程的,最重要的原因还是其良好的抽象性。但对于小型程序和算法来说,面向对象的程序一般会比面向过程的程序慢,所以我们编写程序需要掌握两种思想,发挥出它们的长处。 # 2、类与对象 下面我们正式创建自己的类,这里我们使用Python自定义精灵宝可梦中的小火龙,如代码清单4-1所示。 代码清单4-1 自定义类1 ``` class Charmander: def setName(self,name): self.name = name def getName(self): return self.name def getInfo(self): return self ``` 类的定义就像函数定义,用class语句替代了def语句,同样需要执行class的整段代码这个类才会生效。进入类定义部分后,会创建出一个新的局部作用域,后面定义的类的数据属性和方法都是属于此作用域的局部变量。上面创建的类很简单,只有一些简单的方法。当捕捉到精灵的时候,首先要为其起名字,所以我们先编写函数setName()和getName()。似乎函数中self参数有点奇怪,我们尝试建立具体的对象来探究该参数的作用。 ``` >>>pokemon1 = Charmander() >>>pokemon2 = Charmander() >>>pokemon1.setName('Bang') >>>pokemon2.setName('Loop') >>>print(pokemon1.getName()) Bang >>>print(pokemon2.getName()) Loop >>> print(pokemon1.getInfo()) <__main__.Charmander instance at 0x02F26B98> >>> print(pokemon2.getInfo()) <__main__.Charmander instance at 0x02F26AF8> ``` 创建对象和调用一个函数很相似,使用类名作为关键字创建一个类的对象。实际上Charmander的括号里是可以有参数的,后面我们会讨论到。我们捕捉了两只精灵,一只名字为Bang,另一只为Loop,并且对它们执行getName(),名字正确返回。观察getInfo()的输出,返回的是包含地址的具体对象的信息,可以看到两个对象的地址是不一样的。self的作用与C++的*this指针类似,在调用Charmander的setName和getName函数时,函数都会自动把该对象的地址作为第一个参数传入(该信息包含在参数self中),这就是为什么我们调用函数时不需要写self,而在函数定义时需要把self作为第一个参数。传入对象的地址是相当必要的,如果不传入地址,程序就不知道要访问类的哪一个对象。 类的每个对象都会有各自的数据属性。Charmander类中有数据属性name,这是通过setName()函数中的语句self.name=name创建的。这个语句中的两个name是不一样的,它们的作用域不一样。第一个name通过self语句声明的作用域是类Charmander()的作用域,将其作为pokenmon1的数据属性进行存储,而后面的name的作用域是函数的局部作用域,与参数中的name相同。而后面getName()函数返回的是对象中的name。 # 3、__init__方法 从深一层的逻辑去说,我们捕捉到精灵的那一刻应该已经起好了名字,而并非捕捉后再去设置。所以这里我们需要的是一个初始化的手段。Python中的__init__方法用于初始化类的实例对象。__init__函数的作用一定程度上与C++的构造函数相似,但并不等于。C++的构造函数是使用该函数去创建一个类的示例对象,而Python执行__init__方法时实例对象已被构造出来。__init__方法会在对象构造出来后自动执行,所以可以用于初始化我们所需要的数据属性。修改Charmander类的代码,如代码清单4-2所示。 代码清单4-2 自定义类2 ``` class Charmander: def __init__(self,name,gender,level): self.type = ('fire',None) self.gender = gender self.name = name self.level = level self.status = [10+2*level,5+1*level,5+1*level,5+1*level,5+1*level,5+1* level] #最大HP,攻击,防御,特攻,特防,速度 def getName(self): return self.name def getGender(self): return self.gender def getType(self): return self.type def getStatus(self): return self.status ``` 这里我们增加了几个数据属性:性别、等级、能力、属性。连同前面的名字,都放在__init__方法进行初始化。数据属性是可以使用任意数据类型的,小火龙属性是火,而精灵可能会有两个属性,如小火龙经过两次进化成为喷火龙后,属性变为火和飞行。为保持数据类型的一致性,所以我们使用元组存储,并让小火龙的第二个属性为None。由于小火龙的属性是固定的,所以在__init__的输入参数不需要type。而精灵的能力会随着等级不同而不同,所以在初始化中也需要实现这一点。我们创建实例对象测试代码: ``` >>>pokemon1 = Charmander('Bang','male',5) >>>pokemon2 = Charmander('Loop','female',6) >>>print(pokemon1.getName(),pokemon1.getGender(),pokemon1.getStatus()) Bang male [20, 10, 10, 10, 10, 10] >>>print(pokemon2.getName(),pokemon2.getGender(),pokemon2.getStatus()) Loop female [22, 11, 11, 11, 11, 11] ``` 这时候创建实例对象就需要参数了,实际上这是__init__函数的参数。__init__自动将数据属性进行了初始化,然后调用相关函数能够返回我们需要的对象的数据属性。 # 4、对象的方法 ### 1.方法引用 本节我们详细探讨对象的方法,类的方法和对象的方法是一样。我们在定义类的方法时程序没有为类的方法分配内存,而在创建具体实例对象的程序才会为对象的每个数据属性和方法分配内存。我们已经知道定义类的方法是def定义的,具体定义格式与普通函数相似,只不过类的方法的第一个参数需要为self参数。我们可以用普通函数实现对对象函数的引用: ``` >>>pokemon1 = Charmander('Bang','male',5) >>>getStatus1 = pokemon1.getStatus >>>print(getStatus1()) [20, 10, 10, 10, 10, 10] ``` 虽然这看上去似乎是调用了一个普通函数,但是getStatus1()这个函数是引用pokm-emon1.getStatus()的,意味着程序还是隐性地加入了self参数。 ### 2.私有化 另外我们再谈谈私有化。使用代码清单4-3,我们发现如果要获取对象的数据属性并不需要通过getName(),getType()等方法,直接在程序外部调用数据属性即可: ``` >>> print(pokemon1.type , pokemon1.getType()) ('fire', None) ('fire', None) >>> print(pokemon1.gender , pokemon1.getGender()) male male ``` 虽然这似乎很方便,但是却违反了类的封装原则。对象的状态对于类外部应该是不可访问的。为什么要这样做?我们查看Python的模块的源码时会发现源码里面定义的很多类,模块中的算法通过使用类实现是很常见的,如果我们使用算法时能够随意访问对象中的数据属性,那么很可能在不经意中修改了算法中已经设置的参数,这是十分糟糕的。尽管我们不会刻意这么做,但是这种无意的改动是常有的事。一般封装好的类都会有足够的函数接口供程序员使用,程序员没有必要访问对象的具体数据属性。 为防止程序员无意地修改了对象的状态,我们需要对类的数据属性和方法进行私有化。Python不支持直接私有方式,但可以使用一些小技巧达到私有特性的目的。为了让方法的数据属性或方法变为私有,只需要在它的名字前面加上双下划线即可,修改Charmander类代码,如代码清单4-3所示: 代码清单4-3 自定义类3 ``` class Charmander: def __init__(self,name,gender,level): self.__type = ('fire',None) self.__gender = gender self.__name = name self.__level = level self.__status = [10+2*level,5+1*level,5+1*level,5+1*level,5+1*level,5+1*level] # 最大HP,攻击,防御,特攻,特防,速度 def getName(self): return self.__name def getGender(self): return self.__gender def getType(self): return self.__type def getStatus(self): return self.__status def level_up(self): self.__status = [s+1 for s in self.__status] self.__status[0]+=1 # HP每级增加2点,其余1点 def __test(self): pass ``` ``` >>> pokemon1 = Charmander('Bang','male',5) >>> print(pokemon1.type) Traceback (most recent call last): File "C:/Users/faker/Desktop/class3.py", line 24, in
print pokemon1.type AttributeError: Charmander instance has no attribute 'type' >>>print(pokemon1.getType()) ('fire', None) >>>pokemon1.test() Traceback (most recent call last): File "C:/Users/faker/Desktop/class3.py", line 26, in
pokemon1.test() AttributeError: Charmander instance has no attribute 'test' ``` 现在在程序外部直接访问私有数据属性是不允许的,我们只能通过设定好的接口函数去调取对象的信息。不过通过双下划线实现的私有化实际上是“伪私有化”,实际上我们还是可以做到从外部访问这些私有数据属性。 ``` print(pokemon1._Charmander__type) print(pokemon1._Charmander__name) print(pokemon1._Charmander__gender) print(pokemon1._Charmander__level) ``` 结果为: ``` ('fire', None) Bang male 5 ``` Python使用的是一种name_mangling技术,将__membername替换成_class__mem-bername,在外部使用原来的私有成员时,会提示无法找到,而上面执行pokemon1._Charmander__type是可以访问。简而言之,确保其他人无法访问对象的方法和数据属性是不可能的,但是使用这种name_mangling技术是一种程序员不应该从外部访问这些私有成员的强有力信号。 可以看到代码中还增加了一个函数level_up(),这个函数用于处理精灵升级时能力的提升。我们不应该在外部修改pokemon的status,所以应准备好接口去处理能力发生变化的情景。函数level_up()仅是一个简单的例子,在工业代码中,这样的函数接口是大量的,程序需要对它们进行归类并附上相应的文档说明。 ### 3.迭代器 我们前面接触到的Python容器对象都可以用for遍历,如代码清单4-4所示: 代码清单4-4 迭代器 ``` for element in [1, 2, 3]: print(element) for element in (1, 2, 3): print(element) for key in {'one':1, 'two':2}: print(key) for char in "123": print(char) for line in open("myfile.txt"): print(line) ``` 这种风格十分简洁方便。for语句在容器对象上调用了iter(),该函数返回一个定义了next()方法的迭代器对象,它将在容器中逐一访问元素。当容器遍历完毕,next()找不到后续元素时,next()会引发一个StopIteration异常,告知for循环终止。例如: ``` >>> L = [1 , 2 , 3] >>> it = iter (L) >>> it
>>> it.next() 1 >>> it.next() 2 >>> it.next() 3 ``` 当知道迭代器协议背后的机制后,我们便可以把迭代器加入到自己的类中。我们需要定义一个__iter__()方法,它返回一个有next方法的对象。如果类定义了next(),__iter__()可以只返回self。再次修改类Charmenda的代码,通过迭代器能输出对象的全部信息,如代码清单4-5所示。 代码清单4-5 自定义类4 ``` class Charmander: def __init__(self,name,gender,level): self.__type = ('fire',None) self.__gender = gender self.__name = name self.__level = level self.__status = [10+2*level,5+1*level,5+1*level,5+1*level,5+1*level,5+1*level] self.__info = [self.__name,self.__type,self.__gender,self.__level,self. __status] self.__index = -1 #最大HP,攻击,防御,特攻,特防,速度 def getName(self): return self.__name def getGender(self): return self.__gender def getType(self): return self.__type def getStatus(self): return self.__status def level_up(self): self.__status = [s+1 for s in self.__status] self.__status[0]+=1 # HP每级增加2点,其余1点 def __iter__(self): print('名字 属性 性别 等级 能力') return self def next(self): if self.__index ==len(self.__info)-1: raise StopIteration self.__index += 1 return self.__info[self.__index] ``` # 5、继承 面向对象的编程带来的好处之一是代码的重用,实现这种重用方法之一是通过继承 机制。继承是两个类或多个类之间的父子关系,子类继承了基类的所有公有数据属性和方法,并且可以通过编写子类的代码扩充子类的功能。开个玩笑地说,如果人类可以做到儿女继承了父母的所有才学并加以拓展,那么人类的发展至少是现在的数万倍。继承实现了数据属性和方法的重用,减少了代码的冗余度。 那么我们何时需要使用继承呢?如果我们需要的类中具有公共的成员,且具有一定的递进关系,那么就可以使用继承,且让结构最简单的类作为基类。一般来说,子类是父类的特殊化,如下面的关系: 哺乳类动物——>狗——>特定狗种 特定狗种类继承狗类,狗类继承哺乳动物类,狗类编写了描述所有狗种公有的行为的方法而特定狗种类则增加了该狗种特有的行为。不过继承也有一定弊端,可能基类对于子类也有一定特殊的地方,如某种特定狗种不具有绝大部分狗种的行为,当程序员没有理清类间的关系时,可能使得子类具有了不该有的方法。另外,如果继承链太长的话,任何一点小的变化都会引起一连串变化,我们使用的继承要注意控制继承链的规模。 继承语法:class子类名(基类名1,基类名2,…)基类写在括号里,如果有多个基类,则需要全部都写在括号里,这种情况称为多继承 。在Python中继承有以下一些特点: * 1)在继承中基类初始化方法__init__不会被自动调用。如果希望子类调用基类的__init__方法,需要在子类的__init__方法中显示调用了它。这与C++和C#区别很大。 * 2)在调用基类的方法时,需要加上基类的类名前缀,且带上self参数变量。注意在类中调用该类中定义的方法时不需要self参数。 * 3)Python总是首先查找对应类的方法,如果在子类中没有对应的方法,Python才会在继承链的基类中按顺序查找。 * 4)在Python继承中,子类不能访问基类的私有成员。 我们最后一次修改类Charmander的代码,如代码清单4-6所示: ``` class pokemon: def __init__(self,name,gender,level,type,status): self.__type = type self.__gender = gender self.__name = name self.__level = level self.__status = status self.__info = [self.__name,self.__type,self.__gender,self.__level, self.__status] self.__index = -1 def getName(self): return self.__name def getGender(self): return self.__gender def getType(self): return self.__type def getStatus(self): return self.__status def level_up(self): self.__status = [s+1 for s in self.__status] self.__status[0]+=1 # HP每级增加2点,其余1点 def __iter__(self): print('名字属性 性别 等级 能力') return self def next(self): if self.__index ==len(self.__info)-1: raise StopIteration self.__index += 1 return self.__info[self.__index] class Charmander(pokemon): def __init__(self,name,gender,level): self.__type = ('fire',None) self.__gender = gender self.__name = name self.__level = level # 最大 HP,攻击,防御,特攻,特防,速度 self.__status = [10+2*level,5+1*level,5+1*level,5+1*level,5+1*level,5+1*level] pokemon.__init__(self,self.__name,self.__gender,self.__level,self.__type, self.__status) ``` ``` >>> pokemon1 = Charmander('Bang','male',5) >>> print(pokemon1.getGender()) male >>> for info in pokemon1: print(info) Bang ('fire', None) male 5 [20, 10, 10, 10, 10, 10] ``` 我们定义了Charmander类的基类pokemon,将精灵共有的行为都放到基类中,子类仅仅需要向基类传输数据属性即可。这样做可以很轻松地定义其他基于pokemon类的子类。因为精灵宝可梦的精灵有数百只,使用继承的方法大大减少了代码量,且当需要对全部精灵进行整体修改时仅需修改pokemon类即可。可以看到我们Charmander类的__init__函数中显示调用了pokemon类的__init__函数,并向基类传输数据,这里注意要加self参数。Charmander类没有继承基类的私有数据属性,因此在子类中只有一个self.__type,不会出现因继承所造成的重名情况。为了能更清晰地讲述这个问题,这里再举一个例子,如代码清单4-7所示: 代码清单4-7 私有成员无法继承 ``` class animal: def __init__(self,age): self.__age = age def print2(self): print(self.__age) class dog(animal): def __init__(self,age): animal.__init__(self,age) def print2(self): print(self.__age) a_animal = animal(10) a_animal.print2() #result: 10 a_dog = dog(10) a_dog.print2() # 程序报错, AttributeError: dog instance has no attribute '_dog__age' ```