面向对象编程(OOP)是广泛用于编写应用程序的思想。作为一名数据科学家,您应当能够编写应用程序来处理数据,以及完成更多相关任务。
在本教程中,我将讲解 Python 中面向对象编程的基础知识。您将学习如何创建类并实例化对象,如何定义方法并向其传递参数,以及如何为类添加属性。
要点速览
OOP 围绕类(蓝图)和对象(蓝图的实例)组织代码
使用
class关键字定义类,并在__init__(self)中初始化属性实例方法通过
self访问对象数据,使用点号调用:my_object.method()Python 支持 OOP 的四大支柱:封装、继承、多态和抽象
OOP 促进代码复用,可从单个类模板创建多个对象
什么是 OOP?
面向对象编程(OOP)基于命令式编程范式,通过语句改变程序状态。命令式意味着它侧重于描述程序应如何运行。命令式编程语言的示例包括 C、C++、Java、Go、Ruby 和 Python。
这与声明式编程相对,后者关注程序应该完成什么,而不指定如何完成(例如 SQL、XQuery)。有关范式对比,请参阅我们的函数式编程与 OOP 对照指南。
OOP 使用对象和类的概念。类可被视为对象的“蓝图”。它们可以拥有各自的
- 属性:所具备的特征
- 方法:可以执行的动作
与其他设计模式相比,OOP 有一些关键优势。开发通常更快、更省成本,模块化方法也使软件更易维护。进而,这会带来更高质量的软件,同时也便于通过新方法和新属性进行扩展。
不过,学习曲线会更长。核心概念需要在前期投入更多精力才能充分理解。从计算层面看,OOP 软件更慢,占用更多内存,因为需要编写更多代码行。
OOP 的四大支柱
除了类和方法,OOP 还建立在四大支柱之上,我之前仅作了简单提及。每一项都值得深入探讨,这里先给出快速概览,并附上相应教程链接。
| 支柱 | 含义 | 了解更多 |
|---|---|---|
| 封装 | 将数据与方法打包在一起,并用命名约定控制访问(_private、__name_mangled) | Python 中的封装 |
| 继承 | 创建子类以复用并扩展父类行为 | Python 继承 |
| 多态 | 不同的类共享同一接口,从而使代码可与对象互换协作 | Python OOP 课程 |
| 抽象 | 使用抽象基类以简洁接口隐藏复杂性 | Python 抽象类 |
OOP 示例
一个类的示例是 Dog。别把它当作某只具体的狗,或您的狗。我们是在一般层面描述狗是什么、能做什么。狗通常有 name 和 age;这些是实例属性。狗还会 bark;这是一个方法。
当您谈到一只具体的狗,在编程中就对应一个对象:对象是类的实例化。这是面向对象编程的基本原理。例如,我的狗 Ozzy 属于 Dog 类。他的属性是 name = 'Ozzy' 和 age = '2'。另一只狗会有不同的属性。
Python 中的 OOP
Python 是学习和实践 OOP 最受欢迎的语言之一。下面我将介绍为何 Python 适合面向对象代码,并演示核心语法。
Python 是面向对象的吗?
Python 是一门很好且支持 OOP 的编程语言。您将用它来定义带有属性和方法的类,然后对其进行调用。
与 Java、C++ 或 R 等其他编程语言相比,Python 的关键优势在于其动态特性和高级数据类型。这意味着程序员无需声明变量和参数类型,使得开发更快、代码也比 Java 或 C++ 更易读。
如果您是 Python 新手,请务必查看我们的Python 入门课程。
如何创建类
要在 Python 中定义类,您可以使用class 关键字,后跟类名和冒号。
类内部通常用 def 定义一个 __init__ 方法(一个双下划线方法)。这是一个初始化器,您之后可用它来实例化对象。它类似于 Java 中的构造函数。
当您创建新对象时,__init__ 会自动调用。它接受一个参数:self,指向对象自身。
目前方法体内使用 pass 关键字,因为 Python 期望您在那里写点什么。请记得使用正确的缩进!
class Dog:
def __init__(self):
pass注意:Python 中的 self 相当于 C++ 或 Java 中的 this。
在这个例子里,您已有一个(几乎是空的)Dog 类,但还没有对象。我们来创建一个!
实例化对象
要实例化一个对象,输入类名,后跟一对括号。您可以将其赋给变量,以便跟踪该对象。
ozzy = Dog()
然后打印它:
print(ozzy)
<__main__.Dog object at 0x111f47278>
为类添加属性
打印出 ozzy 后,很明显这个对象是一只狗。但您还没有添加任何属性。我们通过改写为 Dog 类添加名字和年龄:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
您可以看到,该函数现在在 self 之后接收两个参数:name 和 age。它们分别被赋给 self.name 和 self.age。现在您可以创建一个带名字和年龄的新 ozzy 对象:
ozzy = Dog("Ozzy", 2)
要在 Python 中访问对象的属性,可以使用点号语法。即输入对象名,后跟一个点和属性名:
print(ozzy.name)
print(ozzy.age)
Ozzy
2
也可以把它组合成一句更完整的话:
print(ozzy.name + " is " + str(ozzy.age) + " year(s) old.")
Ozzy is 2 year(s) old.
这里使用了 str() 函数,将整数类型的 age 属性转换为字符串,以便在 print() 函数中使用。
在类中定义方法
现在您有了一个 Dog 类,它确实有名字和年龄,您也可以跟踪它们,但它实际上还不会做什么。这时就需要实例方法。您可以重写该类,加入一个 bark() 方法。注意再次使用了 def 关键字,以及 self 参数。
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print("bark bark!")
现在可以在实例化新的 ozzy 对象后,使用点号调用 bark 方法。该方法会在屏幕上打印 "bark bark!"。注意 .bark() 中的括号。调用方法时总是需要括号。本例中是空的,因为 bark() 方法不接收任何参数。
ozzy = Dog("Ozzy", 2)
ozzy.bark()
bark bark!
还记得之前如何打印 ozzy 吗?下面的代码将该功能实现到了 Dog 类中的 doginfo() 方法里。然后您实例化一些具有不同属性的对象并调用它们的方法。
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print("bark bark!")
def doginfo(self):
print(self.name + " is " + str(self.age) + " year(s) old.")
ozzy = Dog("Ozzy", 2)
skippy = Dog("Skippy", 12)
filou = Dog("Filou", 8)
ozzy.doginfo()
skippy.doginfo()
filou.doginfo()
Ozzy is 2 year(s) old.
Skippy is 12 year(s) old.
Filou is 8 year(s) old.
如您所见,您可以用点号在对象上调用 doginfo() 方法。返回的结果现在取决于您正在对哪个 Dog 对象调用该方法。
由于狗会变老,最好能相应地调整它们的年龄。Ozzy 刚过 3 岁生日,我们来修改它的年龄。
ozzy.age = 3
print(ozzy.age)
3
只需为属性赋予一个新值即可。您也可以在 Dog 类中实现一个 birthday() 方法:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print("bark bark!")
def doginfo(self):
print(self.name + " is " + str(self.age) + " year(s) old.")
def birthday(self):
self.age +=1
ozzy = Dog("Ozzy", 2)
print(ozzy.age)
2
ozzy.birthday()
print(ozzy.age)
3
现在,您无需手动修改狗的年龄。每逢生日,直接调用 birthday() 方法即可。
向方法传递参数
您可能希望让我们的狗有个伙伴。由于不是所有的狗都那么合群,因此这应是可选的。
看看下面的 setBuddy() 方法。它像往常一样接收 self,并接收 buddy 作为参数。这里,buddy 将是另一个 Dog 对象。将 self.buddy 属性设为 buddy,并将 buddy.buddy 属性设为 self。
这意味着关系是互相的:您是您伙伴的伙伴。在这里,Filou 将成为 Ozzy 的伙伴,这也意味着 Ozzy 会自动成为 Filou 的伙伴。
您也可以不定义方法,改为手动设置这些属性,但那样每次设置伙伴都要多写一行代码。
注意在 Python 中,您无需指定参数类型。如果这是 Java,就需要这样做。
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print("bark bark!")
def doginfo(self):
print(self.name + " is " + str(self.age) + " year(s) old.")
def birthday(self):
self.age +=1
def setBuddy(self, buddy):
self.buddy = buddy
buddy.buddy = self
现在您可以用点号调用该方法,并向其传入另一个 Dog 对象。在本例中,Ozzy 的伙伴将是 Filou:
ozzy = Dog("Ozzy", 2)
filou = Dog("Filou", 8)
ozzy.setBuddy(filou)
如果您现在想获取有关 Ozzy 的伙伴的一些信息,可以使用两次点号:第一次引用 Ozzy 的伙伴,第二次引用其属性。
print(ozzy.buddy.name)
print(ozzy.buddy.age)
Filou
8
注意,对 Filou 也可以这样做。
print(filou.buddy.name)
print(filou.buddy.age)
Ozzy
2
还可以调用伙伴的方法。传递给 doginfo() 的 self 参数现在是 ozzy.buddy,也就是 filou。
ozzy.buddy.doginfo()
Filou is 8 year(s) old.
Python OOP 示例
一个可能用到 Python 面向对象编程的示例是我们的Python 金融:算法交易教程。在其中,我们讲解如何为股票投资组合设置交易策略。
该交易策略基于股票价格的移动平均。如果满足 signals['short_mavg'][short_window:] > signals['long_mavg'][short_window:],则会产生一个信号。该信号是对股票未来价格变动的预测。
在下面的代码中,您会看到先进行初始化,然后计算移动平均并生成信号。由于这不是面向对象的代码,它就是一大段一次性执行的逻辑。
请注意,我们在示例中使用了 aapl,即苹果公司的股票代码。如果您想对另一只股票进行同样的操作,就必须重写代码。
# Initialize
short_window = 40
long_window = 100
signals = pd.DataFrame(index=aapl.index)
signals['signal'] = 0.0
# Create short simple moving average over the short window
signals['short_mavg'] = aapl['Close'].rolling(window=short_window, min_periods=1, center=False).mean()
# Create long simple moving average over the long window
signals['long_mavg'] = aapl['Close'].rolling(window=long_window, min_periods=1, center=False).mean()
# Create signals
signals['signal'][short_window:] = np.where(signals['short_mavg'][short_window:] > signals['long_mavg'][short_window:], 1.0, 0.0)
# Generate trading orders
signals['positions'] = signals['signal'].diff()
# Print `signals`
print(signals)
在面向对象的方法中,您只需把初始化和信号生成代码写一次。
然后,您可以为每只需要计算策略的股票创建一个新对象,并在其上调用 generate_signals() 方法。请注意,OOP 代码与上面的代码非常相似,只是多了 self。
class MovingAverage():
def __init__(self, symbol, bars, short_window, long_window):
self.symbol = symbol
self.bars = bars
self.short_window = short_window
self.long_window = long_window
def generate_signals(self):
signals = pd.DataFrame(index=self.bars.index)
signals['signal'] = 0.0
signals['short_mavg'] = self.bars['Close'].rolling(window=self.short_window, min_periods=1, center=False).mean()
signals['long_mavg'] = bars['Close'].rolling(window=self.long_window, min_periods=1, center=False).mean()
signals['signal'][self.short_window:] = np.where(signals['short_mavg'][self.short_window:] > signals['long_mavg'][self.short_window:], 1.0, 0.0)
signals['positions'] = signals['signal'].diff()
return signals
现在,您只需用想要的参数实例化一个对象并为其生成信号即可。
apple = MovingAverage('aapl', aapl, 40, 100)
print(apple.generate_signals())
对另一只股票进行同样操作就非常容易了。只需用不同的股票代码实例化一个新对象即可。
microsoft = MovingAverage('msft', msft, 40, 100)
print(microsoft.generate_signals())
结语
我们已经介绍了 Python 中的一些主要 OOP 概念。您现在知道如何声明类和方法、实例化对象、设置其属性以及调用实例方法。这些技能将在您未来的数据科学职业生涯中大有裨益。
若想以更现代的方式定义类,请查看Python 数据类,它能减少样板代码。如果您想进一步扩展继续使用 Python 所需的关键概念,请务必查看我们的Python 进阶课程。
随着程序规模扩大,使用 OOP 的代码复杂度也会增长。您会有不同的类、子类、对象、继承、实例方法等。对于复杂的继承场景,请了解多重继承与 super()。
Python OOP 常见问答
What is object-oriented programming (OOP)?
面向对象编程是一种以“对象”为核心的编程范式,对象可包含数据以及用于操作这些数据的代码。在 OOP 中,对象由称为“类”的模板创建,类定义了所创建对象的属性和行为。OOP 使您能够创建可重用代码,并更贴近地建模现实世界概念,因此被广泛用于各类软件项目。
What are classes and objects in Python?
在 Python 中,类是用于创建对象的模板。它定义了由其创建的对象的属性和行为。对象是类的一个实例,通过像调用函数一样调用类来创建。对象包含类定义的数据和行为,并具有唯一的身份标识。
How do I define a class in Python?
要在 Python 中定义类,使用 class 关键字,后跟类名和冒号。类定义使用缩进,缩进块中包含属于该类的属性和方法(函数)。
How do I create an object from a class in Python?
要从类创建对象,在 Python 中像调用函数那样调用该类,并将任何必要的参数传递给类的构造函数(__init__ 方法)。
What are the four pillars of OOP in Python?
面向对象编程的四大支柱是:封装(将数据与方法打包并限制访问)、继承(允许类从父类继承属性和方法)、多态(使不同类型的对象可被统一对待)、以及抽象(隐藏复杂实现,仅暴露必要特性)。
What is the difference between a class attribute and an instance attribute?
类属性由该类的所有实例共享,直接在类体中定义。实例属性是对象独有的,在 __init__ 方法内使用 self 定义。例如,所有狗都可以共享一个 species 类属性,但每只狗都有自己的 name 实例属性。