概念
谈到面向对象,很多程序员会抛出三个词:封装、继承和多态;或者说抽象、一切都是对象之类的话,然而这会让初学者更加疑惑。下面我想通过一个小例子来说明一下
面向对象一般是和面向过程做对比的,下面是一个简单功能的面向过程和面向对象形式
a = 2# 要实现 a+2
# 面向过程的方法
sum(a, 3)
# 面向对象的方法(这条在R中还不可执行,只是类似这个意思)
a.sum(3)
看上去只是调用的形式不同,本质没有什么差别,不过当代码量比较大的时候,面向对象就会让我们编程的思路更加清晰。我们先根据上面的形式讲解一下面向过程和面向对象的差别。
差别一:侧重点不同
我们可以把调用函数理解为主谓宾的结构
- 面向过程就是我们平时调用的函数,通常是这种形式:动作(主语,宾语) ,动作是主要的,主语和宾语分别作为参数传入进行计算
- 而面向对象的重点则在于这个主语,是这个主语调用了特定的动作,再把宾语作为参数实现运算。
差别二:定义方式不同
- 面向过程只要定义一个函数sum,指定它有两个参数,然后return二者的和即可
- 面向对象则复杂一些。首先要定义一个类,在这个类中定义这个类别可以使用的各种方法(可以理解为函数)。然后产生一个类的实例,用这个实例调用这个方法完成计算
- 举一个通俗的例子,这里的类和我们生活中的类没有什么区别。比如定义一个“鸟”类,再指定这个类有“飞翔”这个方法(即函数、动作)。然后我们抓到一只具体的鸟,这只鸟就是“鸟”类的一个实例,它就可以调用“飞翔”这个方法。如果拉过来一只狗,它不属于“鸟”类,就不能调用“飞翔”这个方法。狗可能属于“狗”类,经过定义就可以调用“叫”这个方法。
- 在sum的例子中,a是一个整数,它是”整数“这个类的一个实例,你也可以定义b=3则是另外一个实例。它们就具备”整数“这个类可以使用的所有方法,比如加一个数,减一个数之类的。而如果 c="hello",它就属于“字符串”这个类,可以调用字符串的方法
- 这样做有一个好处,相当于自动对变量进行分类,每一个变量都是一个对象,属于一个特定的类,它可以调用的方法也都是固定的,这样我们拿到一个字符串,就知道可以对它进行哪些处理。
差异三:调用
在实际使用中,如果要对许多对象进行相同的操作
- 面向过程就是定义一个函数,或者许多函数,然后对每一个对象都套用这个函数即可实现
- 而面向对象则是定义一个类,指定类的方法,然后每一个对象创建为这个类的实例,实例再调用方法进行计算
当这种需求多起来
- 面向过程会出现这种情况,今天对A类数据创建了一些函数,明天对B类数据创建了另外一些函数,可能第二天的函数还调用了第一天的函数,函数全部放在一起,拿到数据看哪个好用就拿来用,这样容易乱套。而且每次拿到一个数据都要审视一下之前的这个函数可以处理这个数据吗,处理完可以得到想要的结果吗
- 而面向对象则每一个类型的对象的方法都放在一起进行管理,都在这个类之下进行定义,这样我们只要看这个对象是这个类的,就自然可以调用这个类的方法。
- 有的人可能会想,这样面向对象岂不是每一个类都要重新实现那些函数,本来可以调用之前定义好了的函数的。然而类的继承很好地解决了这个问题。比如定义了“鸟”和“狗”类,它们都有“吃”这个方法,但是鸟类可以“飞”,狗类可以“跑”,那么“吃”这个方法其实不需要在两个类中分别定义一次。因为可以先定义一个"动物"类,这个类有“吃”“睡”等方法,然后“鸟”和“狗”类分别从这个类继承下来,便获得了“动物”这个类的所有方法,然后它们可以在自己的类中定义自己特定的方法“飞”“跑”等
接下来我们用python下的例子更具体地说明面向对象的好处
方便函数管理
定义三个类
class bird(object): # 定义鸟这个类def __init__(self, name): # 定义类时需要传入的参数,这里指创建实例时需要传入name参数
self.name = name # 将参数赋值给self.name,成为属性,后面定义方法时会调用
def move(self): # 每个类实现一次move方法
print("The bird named",self.name,"is flying")
class dog(object): # 定义狗这个类
def __init__(self, name):
self.name = name
def move(self):
print("The dog named",self.name,"is running")
class fish(object): # 定义鱼这个类
def __init__(self, name):
self.name = name
def move(self):
print("The fish named",self.name,"is swimming")
产生实例
bob = bird("Bob") # 给bob这个变量(对象)传入“姓名”name参数john = bird("John") # 产生两个bird的实例来对比
david = dog("David")
fabian = fish("Fabian")
每个实例分别调用move方法
bob.move() # 1 标号(方便文字说明)john.move() # 2
david.move() # 3
fabian.move() # 4
# 得到结果如下
# The bird named Bob is flying
# The bird named John is flying
# The dog named David is running
# The fish named Fabian is swimming
首先说明一点,创建实例时要传入这个对象的各种属性值,属性用于存储这个对象的数据,也是一个对象区别于其他对象的标准。比如上面bob这个对象的name是Bob,john则是John,他们正因为传入的参数不同才是两个不同的对象,用相同的方法处理后结果才有差异。我们用同一个函数处理不同的数据也体现在这里,数据都是通过参数传入的,定义这个对象时就把数据传入了,之后再调用方法进行处理,其实是方法在定义时调用了这些属性(也就是你创建实例时传入的数据)。
在这个例子中,move方法其实就通过self.name调用了name属性,运行后"Bob"等才会出现在结果中。实际使用时,传入的参数肯定不会这么简单,可能不会是name这样的姓名最后输出出来而已,而是一个数据框,或者一个非常长的待处理字符串,他们都会被后面的方法调用,实现诸如去除缺失值、描述数据、绘图、建模等方法。
从上面的结果中我们可以看到
- 234的对比表明,不同对象调用同一个方法move,却得到不同的结果,“鸟”类都是以"the bird named"开头,“狗”类the dog,“鱼”类fish。这是因为它们调用了不同的函数。这样,一个类似的功能,可以定义一个同名的函数方便记忆,同时在不同类中还可以分别得到妥当的处理
- 12的对比表明,同一类传入不同数据,也可以定制出差异化的结果。这就和一个函数处理不同数据得到不同结果一样
如果使用面向过程的方法,则要如下实现
def movebird(name):print("The bird named",name,"is flying")
def movedog(name):
print("The dog named",name,"is running")
def movefish(name):
print("The fish named",name,"is swimming")
bob = "Bob"
john = "John"
david = "David"
fabian = "Fabian"
movebird(bob)
movebird(john)
movedog(david)
movefish(fabian)
# 得到结果
# The bird named Bob is flying
# The bird named John is flying
# The dog named David is running
# The fish named Fabian is swimming
这样我们拿到一个变量bob,还要分清楚它表示“鸟”还是“狗”,然后在决定用movebird函数还是movedog函数。一般我们不会只实现move这一种功能,倘若有一二十种,四五十个函数堆在那里,得到的数据也有几十个,还要一一对应就很麻烦。除此之外,这些函数可能相互调用,如果想要将这个函数用到其他项目中,还要去找它调用了哪些函数一起复制到新项目中。如果这个代码很久没看了,再回来看更要花费很大的精力来对应。
而面向对象定义这个变量时就是通过类产生实例,要想知道用什么方法处理只要直接去找那个类的定义,甚至在编辑器的代码提示中就会直接列出来供你挑选。
所以面向对象无论在编程处理的过程,还是代码的管理上都有非常大的优势。
数据封装
当我们要定义很多函数要调用相同参数时,面向对象的使用会明显更方便,我们看下面一个例子
定义类如下
class Person:'''细心的读者可能注意到这里的定义和前面形式不一样,没有object,在这里说明一下
python对象系统分为经典类和新式类,py2中不加object默认经典类,加了是新式类
py3中则默认不加也是新式类,所以以后我们定义时都不加object(虽然加也是一样的)'''
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
def description(self):
print("Description: name is %s, age is %s, height is %s"% (self.name,self.age,self.height))
def my(self):
print("My name is %s, and height is %s, and age is %s. "% (self.name,self.height,self.age))
调用实例
bob = Person("Bob",24,170)mary = Person("Mary",10,160)
bob.description()
bob.my()
mary.description()
mary.my()
# Description: name is Bob, age is 24, height is 170
# My name is Bob, and height is 170, and age is 24.
# Description: name is Mary, age is 10, height is 160
# My name is Mary, and height is 160, and age is 10.
我们可以看到,创建对象时将数据传入,之后调用方法则不需要再赋值,每个方法在定义时就已将参数传入
上面的过程我们用面向过程来实现,下面提供两种实现方式,都有一定的弊端
第一种,最直接的传入参数
def description(name, age, height):print("Description: name is %s, age is %s, height is %s"% (name,age,height))
def my(name, age, height):
print("My name is %s, and height is %s, and age is %s. "% (name,height,age))
description("Bob",24,170)
description("Mary",20,160)
my("Bob",24,170)
my("Mary",20,160)
# Description: name is Bob, age is 24, height is 170
# Description: name is Mary, age is 20, height is 160
# My name is Bob, and height is 170, and age is 24.
# My name is Mary, and height is 160, and age is 20.
上面这一种每次调用函数都要传入相同的参数,非常麻烦,如果参数更多,对象更多,则非常麻烦
第二种可以不用每次都传入
bob = dict(name='Bob',age=24,height=170)mary = dict(name='Mary',age=20,height=160)
def my(dict):
print("My name is %s, and height is %s, and age is %s. "% (dict['name'],dict['height'],dict['age']))
def description(dict):
print("Description: name is %s, age is %s, height is %s"% (dict['name'],dict['age'],dict['height']))
my(bob)
description(bob)
my(mary)
description(mary)
# My name is Bob, and height is 170, and age is 24.
# Description: name is Bob, age is 24, height is 170
# My name is Mary, and height is 160, and age is 20.
# Description: name is Mary, age is 20, height is 160
这种方法其实就是把参数变化一下,但是有一个很大的弊端,这两个函数的定义中,默认了传入的字典有name age height这三个键,如果没有就会发生各种错误。所以说这个函数的应用其实非常局限,几乎无法应用在其他地方,这种函数其实比较适合的是封装在一个类里面。
对象操作
有时候我们要实现一个对象的动态过程,即一个对象做了什么,再做什么,面向过程就很难实现了,看下面一个例子
class Individual:def __init__(self,full=10): # 默认值则创建实例时可以不用传入
self.full = full
def fruit(self):
self.full += 1
return self # 使作用完这个方法后还可以调用其他方法
def meat(self):
self.full += 2
return self
def run(self):
self.full -= 3
return self
上面定义了一个个体的类,他有一个属性可以理解成能量,他的方法有吃水果、吃肉、跑步,每一个方法调用后都会改变他的能量值。下面创建一个实例来调用
anyone = Individual()anyone.full # 10
anyone.meat() # 让他吃一次肉
anyone.full # 12
anyone.run() # 让他跑一次
anyone.full # 9
anyone.meat().run() # 吃完再跑
anyone.full # 8
我们还可以进行区分,定义两个类,继承上面的类
class Boy(Individual):'''定义男生一天吃肉跑,再吃再跑再吃'''
def oneday(self):
self.meat().meat().run().meat().fruit().run().meat()
print(self.full)
class Girl(Individual):
'''定义女生每天吃的少但是不跑'''
def oneday(self):
self.meat().fruit()
print(self.full)
上面两个类定义了男生、女生一天的动作,通过调用oneday返回他们做一天后剩余的能量
bob = Boy()bob.oneday() # 13
mary = Girl()
mary.oneday() # 13
上面这个过程,如果使用面向过程的方法,则需要对每个对象定义一个专门表示他们能量的变量,再定义函数对这些值进行修改,这会造成变量和参数的极大冗余,这里就不再举例子
用面向对象很多时候调用方法的时候都非常符合人的思维习惯,如上面的做完一件事再做一件事就用.连着写下去。这样的类还有很多,比如下面的字符串例子
- 面向对象 a.casefold().join(['a','b']).strip().find('c')
- 面向过程 find(strip(join(casefold(a),['a','b'])),'c')
假设上面的函数都是可用的,面向对象一步一步非常清晰:大小写转换-连接ab-去除两边空格-找到'c',而面向过程则层层嵌套,代码非常不易读,如果不嵌套则可能产生许多无用的中间变量(虽然R里面有管道操作可以解决这个问题,但是不是所有面向过程的语言都有实现这样的功能的)
总结面向对象编程的好处
- 将对象进行分类,分别封装它们的数据和可以调用的方法,方便了函数、变量、数据的管理,方便方法的调用(减少重复参数等),尤其是在编写大型程序时更有帮助。
- 用面向对象的编程可以把变量当成对象进行操作,让编程思路更加清晰简洁,而且减少了很多冗余变量的出现