一、认识面向对象编程
(一)面向对象编程
OOP,即面向对象编程(或 “面向对象程序设计” ,Object Oriented Programming)。类和对象是OOP中的两个关键内容,在面向对象编程中,以类来构造现实世界中的事物情景,再基于类创建对象来进一步认识、理解、刻画。根据类来创建的对象,每个对象都会自动带有类的属性和特点,然后可以按照实际需要赋予每个对象特有的属性,这个过程被称为类的实例化。
描述创建数据或逻辑的简单抽象层(抽象的直接表现形式通常为类)。抽象指对现实世界的事物,行为和特征建模,建立一个相关的数据集用于描绘程序结构,从而实现这个模型。抽象不仅包括这种模型的数据属性,还定义了这些数据的接口。从面向对象设计(Object Oriented Design,简称OOD)角度去看,如果类是从现实对象抽象而来的,那么抽象类就是基于类抽象而来的,可以进行相似编码,或者编入与对象交互的对象中。从实现角度来看,抽象类与普通类的不同之处在于:抽象类中只能有抽象方法(没有实现功能),该类不能被实例化,只能被继承,且子类必须实现抽象方法。
(二)面向对象编程发展
OO方法(Object Oriented Method,面向对象方法,简称 OO 方法)在软件开发过程中以 “对象” 为中心,用面向对象的思想来指导开发活动的系统方法。20世纪50年代后期,在编写大型程序时,常出现相同变量名在程序不同部分发生冲突的问题。对于这个问题,ALGOL 语言的设计者在 ALGOL60 中用 “Begin……End” 为标识的程序设计形成局部变量,避免它们与程序中其他同名变量相冲突。这是编程语言中首次提供封装的尝试,后来此结构广泛用于高级语言如 Pascal、Ada、C 之中。
60年代中后期,在 ALGOL 基础上研制开发了 Simula 语言,提出了对象的概念,并使用了类,也支持类继承,出现了面向对象程序设计的雏形。70年代,经典的 Smalltalk 语言诞生,它以 Simula 的类为核心概念,以 Lisp 语言为主要内容。由于 Smautalk 持续不断的改进,引入了对象、对象类、方法、实例等概念和术语,采用动态联编和单继承机制,以至于现在都将这一语言视为面向对象的基础。
正是通过 Smalltalk 不断的改进与推广应用,人们才发现面向对象方法具有模块化、信息封装与隐蔽、抽象性、继承性、多态性等独特之处,为研制大型软件、提高软件可靠性、可重用性、可扩充性和可维护性提供了有效的手段和途径。
在过去几年中,Java 语言成为了广为应用的语言,除了它与C语言语法上的近似,还有面向对象编程的强大一面,即 Java 的可移植性。在近几年计算机语言发展中,一些既支持面向过程程序设计(该怎么做),又支持面向对象程序设计(对象该怎么做)的语言崭露头脚,如 Python、Ruby 等。
1986年在美国举行了首届 “面向对象编程、系统、语言和应用(OOPSLA'86)” 国际会议,使面向对象受到世人瞩目,其后每年一届,这标志着面向对象方法的研究已普及到全世界。面向对象方法已被广泛应用于程序设计语言、数据库、设计方法学、人机接口、操作系统、分布式系统、人工智能、实时系统、计算机体系结构以及并发工程、综合集成工程等众多领域,同时在其领域应用都得到了很大的发展。例如,现代的面向对象程序设计方法使得设计模式的用途、契约式设计和建模语言(如UML)技术也得到了一定提升。
(三)面向对象编程实例
例如五子棋,面向过程的设计思路就是首先分析问题的步骤:开始游戏;黑子先走;绘制画面;判断输赢;轮到白子;绘制画面;判断输赢;返回步骤2;输出最后结果。把每个步骤分别用函数来实现,问题就解决了。而面对对象的设计则是从另一种思路来解决问题。黑白双方,这两方的行为是一模一样的;棋盘系统,负责绘制画面;规则系统,负责判断诸如犯规、输赢等。
第一类对象(玩家对象)负责接受用户输入信息,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的输入就要负责在画面上显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以明显的看出,面向对象是以功能来划分问题,而不是循环步骤。同样是绘制棋局,在面向过程的设计中,需要多个步骤执行该任务。但这样很可能导致不同步骤的绘制棋局程序不同,因为设计人员会根据实际情况对绘制棋局的程序进行简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证绘制棋局的统一。
(四)面向对象编程介绍
(1)面向过程:根据业务逻辑从上到下写垒代码;
(2)函数式:将某功能代码封装到函数中,无需重复编写整个功能代码的实现,仅调用即可;
(3)面向对象:对函数进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程;
在面向过程程序设计中,问题被看作一系列需要完成的任务,解决问题的焦点集中于函数。面向对象有如下优点:
二、类的定义和方法
(一)类
就相当于制造飞机时的图纸,用它来进行创建的飞机就相当于对象。
具有相似内部状态和运动规律的实体的集合(或统称、抽象)。
具有相同属性和行为事物的统称。
类是抽象的,在使用的时候通常会找到这个类的一个具体的存在,使用这个具体的存在。一个类可以找到多个对象。
(二)类的构成
类(Class) 由3个部分构成
(1)类的名称:类名
(2)类的属性:一组数据
(3)类的方法:允许对进行操作的方法 (行为)
1)人的类设计:
类名: 人(Person)
属性: 身高(height)、年龄(age)
方法(行为/功能): 跑(run)、打架(fight)
2)狗类的设计
类名: 狗(Dog)
属性: 品种 、毛色、性别、名字
方法(行为/功能): 叫 、跑、咬人、吃、摇尾巴
(三)类的定义
定义一个类,格式如下:
class 类名: 属性列表 方法列表
# 定义一个Car类 class Car: # 属性 wheelNum = 4 color = 'red' # 方法 def getCarInfo(self): print('车轮子个数:%d, 颜色%s'%(self.wheelNum, self.color)) def run(self): print('车在奔跑...')
(四)绑定self
Python 的类的方法和普通的函数有一个很明显的区别,类的方法必须有个额外的第一个参数(self),但在调用这个方法的时候不必为这个参数赋值。Python 类方法的这个特别参数指代的是对象本身,而按照 Python 惯例,它用 self 来表示。
self 代表当前对象的地址,self 能避免非限定调用造成的全局变量。当调用 sleep 等函数时,会自动把该对象的地址作为第一个参数传入;如果不传入地址,程序将不知道该访问哪个对象。 self 名称也不是必须的,在 Python 中 self 不是关键词,可以定义成 a 或 b 或其它名字。
(五)类的专有方法
类的专有方法 | 功能 | 类的专有方法 | 功能 |
__init__ | 构造函数,在生成对象时调用 | __call__ | 函数调用 |
__del__ | 析构函数,释放对象时使用 | __add__ | 加运算 |
__repr__ | 打印,转换 | __sub__ | 减运算 |
__setitem__ | 按照索引赋值 | __mul__ | 乘运算 |
__getitem__ | 按照索引获取值 | __div__ | 除运算 |
__len__ | 获得长度 | __mod__ | 求余运算 |
__cmp__ | 比较运算 | __pow__ | 乘方 |
三、对象的属性和方法
(一)类和对象
类就是创建对象的模板。
(二)创建对象
创建对象的格式为:
对象名 = 类名()
# 创建对象 BMW = Car() # 访问属性 Print( '车轮子的数量为:',BMW.wheelNum) # 调用对象的getCarInfo方法 BMW.getCarInfo() # 调用对象的run方法 BMW.run()
(三)self
# 定义一个类 class Animal: # 方法 def setName(self, name): self.name = name def printName(self): print( '名字为:', self.name)
# 定义一个函数 def myPrint(animalName): animalName.printName() dog1 = Animal() dog1.setName('东东') myPrint(dog1)
(四)构造
构造器方法的格式 def __init__():
构造器方法调用 class Car:
# 构造器方法 def __init__(self): self.wheelNum = 4 self.color = 'blue' # 方法 def run(self): print( '车在跑,目标:夏威夷') # 创建对象 BMW = Car() print('车的颜色为:%s'%BMW.color) print('车轮子数量为:%d'%BMW.wheelNum)
在创建完对象后 __init__() 方法已经被默认的执行了,那么能否让对象在默认调用 __init__() 方法的时候传递一些参数呢?如果可以,那怎样传递呢?
# 定义类 class Cat(): """再次模拟猫咪的简单尝试""" def __init__(self,name,age): # 构造器方法 self.name = name # 属性 self.age = age def sleep(self): """模拟猫咪被命令睡觉""" print('%d岁的%s正在沙发上睡懒觉。'%(self.age, self.name)) def eat(self,food): """模拟猫咪被命令吃东西""" self.food = food print(‘%d岁的%s在吃%s'%(self.age, self.name,self.food))
创建对象时,默认调用构造方法;当删除一个对象时,同样也会默认调用一个方法,这个方法为析构方法析构方法 (__del__())
class Animal(): # 构造方法 def __init__(self): print( '---构造方法被调用---') # 析构方法 def __del__(self): print( '---析构方法被调用---') # 创建对象 dog = Animal() # 删除对象 del dog
(五)对象的方法
类的方法和对象的方法是一样的。在定义类的方法时程序没有为类的方法分配内存,而在创建具体实例对象的程序才会为对象的每个数据属性和方法分配内存。
cat1 = Cat('Tom', 3) sleep = cat1.sleep print(sleep()) 3岁的Tom正在沙发上睡懒觉。
cat2 = Cat('Jack',4) eat = cat2.eat print(eat('fish')) 4岁的Jack在吃fish。
虽然看上去似乎是调用了一个普通函数,但是 sleep 函数和 eat 函数是引用 cat1.sleep() 和 cat2.eat() 的,意味着程序还是隐性地加入了 self 参数。
(六)私有化
要获取对象的数据属性并不需要通过 sleep,eat 等方法,直接在程序外部调用数据属性即可。
print(cat1.age) 3
尽管这似乎很方便,但是却违反了类的封装原则。对象的状态对于类外部应该是不可访问的。查看 Python 模块代码时会发现源码里面定义的很多类,模块中的算法通过使用类实现是很常见的,如果使用算法时能够随意访问对象中的数据属性,那么很可能在不经意中修改了算法中已经设置的参数,这是很麻烦的。一般封装好的类都会有足够的函数接口供程序员使用,程序员没有必要访问对象的具体数据属性。
为防止程序员无意中修改对象的状态,需要对类的数据属性和方法进行私有化。Python 不支持直接私有方式,但可以使用一些小技巧达到私有特性的目的。为了让方法的数据属性或方法变为私有,只需要在它的名字前面加上双下划线即可。
四、迭代器
(一)生成迭代器
迭代是 Python 最强大的功能之一,是访问集合元素的一种方式。之前接触到的 Python 容器对象都可以用 for 遍历。
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)
迭代器有两个基本的方法:iter 函数和 next 函数。for 语句在容器对象上调用了 iter 函数,该函数返回一个定义 next 函数的迭代对象,它将在容器中逐一访问元素。当容器遍历完毕,next 函数找不到后续元素时,会引发一个 StopIteration 异常,告知 for 循环终止。
L = [1,2,3] it = iter(L) it next(it) next(it) next(it) next(it) Traceback (most recent call last): File "<ipython-input-9-2cdb14c0d4d6>", line 1, in <module> next(it) StopIteration
当知道迭代器协议的机制后,就可以把迭代器加入到自己的类中。需要定义一个 __iter__ 函数,它返回一个有 next 方法的对象。如果类定义了 next 函数,__iter__ 函数可以只返回 self 。仍以 Cat 类为例,通过迭代器能输出对象的全部信息。
(二)返回迭代器——yield
在 Python 中,使用生成器(generator)可以很方便的支持迭代器协议。生成器是一个返回迭代器的函数,它可以通过常规的 def 语句来定义,但是不用 return 返回,而是用 yield 一次返回一个结果。不像一般的函数会生成值后退出,生成器函数在生成值后会自动挂起并暂停执行状态并保存状态信息,这些信息在函数恢复时将再度有效。通过在每个结果之间挂起和继续它们的状态来自动实现迭代协议。
这里用一个实例(yield实现斐波那契数列)来区分有 yield 和没有 yield 的情况,对生成器进一步了解。
import sys def fibonacci(n,w=0): # 生成器函数——斐波那契 a, b, counter = 0, 1, 0 while True: if (counter > n): return yield a a, b = b, a + b print('%d,%d' % (a,b)) counter += 1 f = fibonacci(10,0) # f 是一个迭代器,由生成器返回生成 while True: try: print (next(f), end=" ") except : sys.exit() 0 1,1 1 1,2 1 2,3 2 3,5 3 5,8 5 8,13 8 13,21 13 21,34 21 34,55 34 55,89 55 89,144
import sys def fibonacci(n,w=0): # 生成器函数——斐波那契 a, b, counter = 0, 1, 0 while True: if (counter > n): return # yield a # 不执行yield语句 a, b = b, a + b print('%d,%d' % (a,b)) counter += 1 f = fibonacci(10,0) # f 是一个迭代器,由生成器返回生成 while True: try: print (next(f), end=" ") except : sys.exit() 1,1 1,2 2,3 3,5 5,8 8,13 13,21 21,34 34,55 55,89 89,144
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值。并在下一次执行 next 函数时从当前位置继续运行。
简而言之,包含 yield 语句的函数会被特地编译成生成器。当函数被调用时,他们返回一个生成器对象,这个对象支持迭代器接口。
五、继承父类属性和方法
面向对象的编程带来好处之一是代码的重用,实现这种重用方法之一是通过继承机制。 继承是两个类或多个类之间的父子关系,子类继承了基类的所有公有数据属性和方法,并且可以通过编写子类的代码扩充子类的功能。 继承实现了数据属性和方法的重用,减少了代码的冗余度。
(一)继承
那么我们何时需要使用继承呢?如果我们需要的类中具有公共的成员,且具有一定的递进关系,那么我们的就可以使用继承,且让结构最简单的类作为基类。
一般来说,子类是父类的特殊化,如下面的关系:哺乳类动物 ——> 狗 ——> 特定狗种
在程序中,继承描述的是事物之间的所属关系,例如猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物;同理,波斯猫和巴厘猫都继承自猫,而沙皮狗和斑点狗都继承足够,如下所示:
继承语法:class 子类名(基类名1, 基类名2, …) 基类写在括号里,如果有多个基类,则需要全部都写在括号里,这种情况称为多继承。在 Python 中继承有下面一些特点:
(1)在继承中基类初始化方法 __init__ 不会被自动调用。如果希望子类调用基类的 __init__ 方法,需要在子类的 __init__ 方法显示调用它。这与C++和C#区别很大。
(2)在调用基类的方法时,需要加上基类的类名前缀,且带上 self 参数变量。注意在类中调用在该类定义的方法是不需要 self 参数的。
(3)Python 总是首先查找对应类的方法,如果在子类没有对应的方法,Python 才会在继承链的基类中按顺序查找。
(4)在 Python 继承中,子类不能访问基类的私有成员。
class Cat(): def __init__(self): self.name = '猫' self.age = 4 self.info = [self.name,self.age] self.index = -1 def run(self): print( self.name,'--在跑') def getName(self): return self.name def getAge(self): return self.age 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 Bosi(Cat): def setName(self, newName): self.name = newName def eat(self): print( self.name,'--在吃') # 创建对象 bs = Bosi() # 继承父类的属性和方法 print( 'bs的名字为:',bs.name) print( 'bs的年龄为:',bs.age) print(bs.run()) # 子类的属性和方法 bs.setName('波斯猫') bs.eat() # 迭代输出父类的属性 iterator = iter(bs.next,1) for info in iterator: print(info)
代码中定义了 Bosi 类的父类 Cat ,将猫共有的属性和方法都放到父类中,子类仅仅需要向父类传输数据属性。这样做可以很轻松地定义其他基于 Cat 类的子类。因为假如有数百只猫,使用继承的方法可以大大减少代码量,且当需要对全部猫整体修改时,仅修改 Cat 类即可。Bosi 类的 init 函数中显示调用了 Cat 类的 init 函数,并向父类传输数据,这里注意要加 self 参数。
(二)多继承
如果有多个父类,则父类名需要全部写在括号里,这种情况称为多继承,格式为 Class 子类名(父类名1,父类名2,…),示例代码如下。
# 定义一个父类 class A(object): def __init__(self): print (" ->Input A") print (" <-Output A") # 定义一个子类 class B(A): def __init__(self): print (" -->Input B") A.__init__(self) print (" <--Output B") # 定义另一个子类 class C(A): def __init__(self): print (" --->Input C") A.__init__(self) print (" <---Output C") # 定义一个子类 class D(B, C): def __init__(self): print ("---->Input D") B.__init__(self) C.__init__(self) print ("<----Output D") d = D()# python中是可以多继承的,父类中的方法、属性,子类会继承。 issubclass(C,B) # 判断一个类是不是另一个类的子孙类 issubclass(C,A)
实现继承之后,子类将继承父类的属性,也可以使用内建函数 issubclass 来判断一个类是不是另一个类的子孙类,前项参数为子类,后项参数为父类。
面向对象的三大特征:重载、封装和多态。
(三) 重载
所谓重载,就是子类中有一个和父类名字相同的方法,子类中的方法会覆盖父类中同名的方法。代码如下。
class Cat: def sayHello(self): print("喵-----1") class Bosi(Cat): def sayHello(self): print("喵喵----2") bosi = Bosi() bosi.sayHello() # 子类中的方法会覆盖掉父类中同名的方法
(四)封装
既然 Cat 实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在 Cat 类的内部定义访问数据的函数。这样,就把数据给 “封装” 起来了。
封装(Encapsulation)就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体(即类);封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是通过外部接口,特定的访问权限来使用类即可。简而言之,就是将内容封装到某个地方,以后再去调用被封装在某处的内容。
(五)多态
多态性(Polymorphism)是对象通过他们共同的属性和动作来操作及访问,允许将父对象设置成和一个或多个它的子对象相等的技术。多态性使得能够利用同一类(父类)类型的指针来引用不同类的对象,以及根据所引用对象的不同,以不同的方式执行相同的操作。
Python 是动态语言,可以调用实例方法,不检查类型,只要方法存在,参数正确就可以调用,这就是与静态语言(例如Java)最大的差别之一。表明了动态(运行时)绑定的存在,允许重载或运行时类型确定和验证。