面向对象编程
https://m.toutiao.com/is/JnNfhMM/
面向对象编程(OOP)对于初学者来说可能是一个很难理解的概念。很多书籍都是从解释OOP开始,讨论三大术语:封装、继承和多态性,但是解释的效果往往让人失望。
本文希望让程序员、数据科学家和python爱好者们更容易理解这个概念。我们去掉所有的行话,通过一些例子来做解说。
这篇文章是关于解释OOP的外行方式。
什么是对象和类
简单地说,Python中的一切都是对象,类是对象的蓝图。所以当我们写下:
a = 2b = 'Hello!'
我们正在创建一个int类的对象a,该对象的值为2,str类的对象b的值为“Hello!”(在默认情况下,用两个引号来提供字符串)。
另外,我们常常无意识地使用到了类和对象的概念,例如在使用scikit-learn模型时,我们实际上是在使用一个类。
clf = RandomForestClassifier()clf.fit(X,y)
这里的分类器clf是一个对象,fit是在RandomForestClassifier类中定义的一个方法。
为什么要使用类
为什么我们经常使用类呢?我们可以用函数实现同样的功能吗?
可以。但是与函数相比,类为我们提供了更多功能。举个例子,str类有很多为对象定义的函数,只需按tab键就可以访问这些函数。我们也可以编写这些函数,但是只按tab键不能使用自己编写的函数。
类的这个属性被称为封装。封装是指将数据与操作该数据的方法捆绑在一起,或者限制对对象某些组件的直接访问。
所以这里str类绑定了数据(“Hello!”)以及所有对数据进行操作的方法。同样,'RandomForestClassifier’类将所有的方法(fit、predict等)捆绑在一起。
除此之外,使用类还可以使代码更加模块化和易于维护。假设我们要创建一个像Scikit-Learn这样的库,就需要创建许多模型,每个模型都有一个fit和predict方法,如果不使用类,我们需要为每个模型提供许多函数,例如:
RFCFitRFCPredictSVCFitSVCPredictLRFitLRPredict and so on.
这种代码结构简直是一场噩梦,因此Scikit Learn将每个模型定义为一个具有fit和predict方法的类。
创建类
现在我们已经了解了为什么要使用类,以及它们为何如此重要。那么如何开始使用它们呢?创建一个类非常简单,下面是编写任何类的样板代码:
class myClass: def __init__(self, a, b): self.a = a self.b = b def somefunc(self, arg1, arg2): # 这里有些代码
这里有很多新的关键字。主要是class
、__init__
和self
。这些是什么呢?
假设你在一家有很多账户的银行工作。我们可以创建一个名为account的类,用于处理任何帐户。例如,下面我创建了一个基本的帐户,它为用户存储数据,即帐户名和余额,它还为我们提供了两种银行存款/取款的方法。请通读一遍以下代码,它遵循与上面代码相同的结构。
class Account: def __init__(self, account_name, balance=0): self.account_name = account_name self.balance = balance def deposit(self, amount): self.balance += amount def withdraw(self,amount): if amount <= self.balance: self.balance -= amount else: print('Cannot Withdraw amounts as no funds!!!')
我们使用以下方法创建一个名为Rahul、金额为100的帐户:
myAccount = Account('Rahul',100)
使用以下方法访问此帐户的数据:
但是,如何将这些属性balance和account_name分别设置为100和“Rahul”?我们从来没有调用过__init__
方法,为什么对象会获得这些属性?答案是,只要我们创建对象,它就会运行。因此,当我们创建myAccount时,它会自动运行函数__init__
。
现在让我们试着存一些钱到我们的账户里:
我们的余额上升到200英镑。你有没有注意到,函数deposit需要两个参数,即self和amount,但我们只提供了一个参数,而且仍然有效。
那么这个self是什么?下面我调用属于类account的同一个函数deposit,并向它提供myAccount对象和amount。现在函数需要两个参数。
我们的账户余额如预期增加了100。所以这是我们调用的同一个函数。只有self和myAccount是完全相同的对象时,才会发生这种情况。Python为函数调用提供与参数self相同的对象myAccount。这就是为什么self.balance在函数定义中真正指的是myAccount.balance.
但是仍然存在一些问题
我们知道如何创建类,但是还有一个重要的问题我还没有提到。
假设你正在与苹果iPhone部门合作,且必须为每种iPhone型号创建一个不同的类。对于这个例子,假设我们的iPhone的第一个版本目前只做一件事——打电话并存储。可以这样写:
class iPhone: def __init__(self, memory, user_id): self.memory = memory self.mobile_id = user_id def call(self, contactNum): # 这里有些实现
现在,苹果计划推出iPhone1,这款iPhone机型引入了一项新功能——拍照功能。一种方法是复制粘贴上述代码并创建一个新的类iPhone1,如下所示:
class iPhone1: def __init__(self, memory, user_id): self.memory = memory self.mobile_id = user_id self.pics = [] def call(self, contactNum): # 这里有些实现 def click_pic(self): # 这里有些实现 pic_taken = ... self.pics.append(pic_taken)
但正如你所看到的,这是大量不必要的代码重复(上面用粗体显示),Python有一个消除代码重复的解决方案。编写iPhone1类的一个好方法是:
Class iPhone1(iPhone): def __init__(self,memory,user_id): super().__init__(memory,user_id) self.pics = [] def click_pic(self): # 这里有些实现 pic_taken = ... self.pics.append(pic_taken)
这就是继承的概念,继承是将一个对象或类基于另一个保留类似实现的对象或类的机制。简单地说,iPhone1现在可以访问类iPhone中定义的所有变量和方法。
在本例中,我们不必进行任何代码复制,因为我们已经从父类iPhone继承(获取)了所有方法。因此,我们不必再次定义调用函数。另外,我们不使用super在函数中设置mobile_uid和内存。
super().__init__(memory,user_id)
是什么?
在现实生活中,你的初始函数不是这些漂亮的函数。你将需要在类中定义许多变量/属性,并且复制并粘贴子类(这里是iphone1),会很麻烦。因此存在super()
,这里super().__init__()
实际上是调用父iPhone类的__init__
方法。因此当类iPhone1的__init__
函数运行时,它会自动使用父类的__init__
函数设置类的memory和user_id。
在ML/DS/DL中的哪里可以看到?下面我们创建PyTorch模型,此模型继承了nn.Module类,并使用super调用该类的__init__
函数。
class myNeuralNet(nn.Module): def __init__(self): super().__init__() # 在这里定义所有层 self.lin1 = nn.Linear(784, 30) self.lin2 = nn.Linear(30, 10) def forward(self, x): # 在此处连接层输出以定义前向传播 x = self.lin1(x) x = self.lin2(x) return x
那么多态又是什么?看下面的类:
import mathclass Shape: def __init__(self, name): self.name = name def area(self): pass def getName(self): return self.nameclass Rectangle(Shape): def __init__(self, name, length, breadth): super().__init__(name) self.length = length self.breadth = breadth def area(self): return self.length*self.breadthclass Square(Rectangle): def __init__(self, name, side): super().__init__(name,side,side)class Circle(Shape): def __init__(self, name, radius): super().__init__(name) self.radius = radius def area(self): return pi*self.radius**2
这里我们有基类Shape和其他派生类-Rectangle和Circle。另外,看看我们如何在Square类中使用多个级别的继承,Square类是从Rectangle派生的,而Rectangle又是从Shape派生的。每个类都有一个名为area的函数,它是根据形状定义的。
因此,通过Python中的多态性,一个同名函数可以执行多个任务。事实上,这就是多态性的字面意思:“具有多种形式的东西”。所以这里我们的函数area有多种形式。
多态性与Python一起工作的另一种方式是使用isinstance方法。因此,使用上面的类,如果我们这样做:
对象mySquare的实例类型是方形、矩形和形状,因此对象是多态的,有很多好的特性。例如,我们可以创建一个与Shape对象一起工作的函数,它将通过使用多态性完全处理任何派生类(Square、Circle、Rectangle等)。
更多信息
为什么有些函数名或属性名以单下划线和双下划线开头?有时我们想让类中的属性和函数私有化,而不允许用户看到它们,这是封装的一部分,我们希望“限制对对象某些组件的直接访问”。例如,假设我们不想让用户看到我们的iPhone创建后的memory(RAM)。在这种情况下,我们使用变量名中的下划线创建属性。
因此,当我们以下面的方式创建iPhone类时,你将无法访问你的memory或iphone私有函数,因为该属性现在使用_
。
但你仍然可以使用(尽管不建议使用)更改变量值。
你还可以使用私有函数myphone._privatefunc()
。如果要避免这种情况,可以在变量名前面使用双下划线。例如,在调用print(myphone.__memory)
下面抛出一个错误。此外,你无法使用myphone更改对象的内部数据myphone.__memory = 1
。
但是,正如你所见,你可以在类定义中的函数setMemory中访问和修改self.__memory
。
结论
希望本文对你理解类很有用。总结一下在这篇文章中我们学习的OOP和创建类以及OOP的各种基础知识:
封装:对象包含自身的所有数据;
继承:创建一个类层次结构,其中父类的方法传递给子类;
多态:函数有多种形式,或者对象可能有多种类型。
我们以一个练习结束本文,让你去实现:创建一个类,使你可以使用体积和曲面面积管理三维对象(球体和立方体)。基本样板代码如下所示:
import mathclass Shape3d: def __init__(self, name): self.name = name def surfaceArea(self): pass def volume(self): pass def getName(self): return self.nameclass Cuboid(): passclass Cube(): passclass Sphere(): pass
如果你想了解更多关于Python的知识,可以参考密歇根大学(universityofmichigan)的一门关于学习中级Python的优秀课程:https://bit.ly/2XshreA。