面向对象编程#

软件工程的目标是控制复杂性,而不是制造复杂性。

—Pamela Zave

面向对象编程(Object-Oriented Programming, OOP)是当今最流行的编程范式之一。如果使用得当,它与过程式编程(Procedural Programming)相比具有许多优势。在许多情况下,OOP 似乎特别适合金融建模和实现金融算法。然而,也有许多批评者对其中的某些方面甚至整个范式持怀疑态度。本章采取中立立场,将 OOP 视为一种重要工具,它可能不是解决每个问题的最佳方法,但应该是从事金融工作的程序员和宽客(Quants)应该掌握的工具。

使用 OOP 会带来一些新的语言术语。就本书和本章而言,最重要的术语如下(更多内容将在后面介绍):

类(Class)#

某种类型对象的抽象定义。例如,人类。

对象(Object)

类的一个实例。例如,Sandra。

属性(Attribute)#

类的一个特征(类属性)或类实例的特征(实例属性)。例如,属于哺乳动物、男性或女性,或眼睛的颜色。

方法(Method)#

类或其实例可以实现的操作。例如,行走。

参数(Parameter)#

方法接受的输入,用于影响其行为。例如,三步。

实例化(Instantiation)

基于抽象类创建特定对象的过程。

转换成 Python 代码,实现人类示例的简单类可能如下所示:

In [1]: class HumanBeing(object):
    def__init__(self, first_name, eye_color):
    self.first_name = first_name
    self.eye_color = eye_color
    self.position = 0
    def walk_steps(self, steps):
    self.position += steps

类定义语句;self 引用当前类的实例。

实例化期间调用的特殊方法。

用参数值初始化的名字属性。

用参数值初始化的眼睛颜色属性。

初始化为 0 的位置属性。

定义行走的方法,steps 为参数。

根据 steps 值改变位置的代码。

基于类定义,可以实例化并使用新的 Python 对象:

In [2]: Sandra = HumanBeing('Sandra', 'blue')
In [3]: Sandra.first_name
Out[3]: 'Sandra'
In [4]: Sandra.position
Out[4]: 0
In [5]: Sandra.walk_steps(5)
In [6]: Sandra.position
Out[6]: 5

实例化。

访问属性值。

调用方法。

访问更新后的位置值。

支持使用 OOP 的若干人为因素:

自然思维方式#

人类思维通常围绕现实世界或抽象对象展开,如汽车或金融工具。OOP 适合用其特征来建模此类对象。

降低复杂性#

通过不同的方法,OOP 有助于降低问题或算法的复杂性,并逐步建立模型。

更友好的用户界面#

OOP 在许多情况下允许更好的用户界面和更紧凑的代码。例如,在查看 NumPy 的 ndarray 类或 pandas 的 DataFrame 类时,这一点变得很明显。

Python式的建模方式#

不论 OOP 的利弊如何,它只是 Python 中的主导范式。这就是"Python 中一切都是对象"这一说法的来源。OOP 还允许程序员构建自定义类,其实例的行为与标准 Python 类的每个实例一样。

支持使用 OOP 的若干技术因素:

抽象(Abstraction)#

使用属性和方法可以构建抽象、灵活的对象模型,关注相关的内容,忽略不需要的内容。在金融领域,这可能意味着有一个通用类来以抽象方式建模金融工具。这样的类的实例将是具体的金融产品,例如由投资银行设计提供的。

模块化(Modularity)#

OOP 简化了将代码分解为多个模块的过程,这些模块随后被链接在一起形成完整的代码库。例如,对股票上的欧式期权进行建模可以通过一个类实现,也可以通过两个类来实现,一个用于标的股票,一个用于期权本身。

继承(Inheritance)#

继承是指一个类可以从另一个类继承属性和方法的概念。在金融领域,从一般的金融工具开始,下一层可以是一般的衍生工具,然后是欧式期权,然后是欧式看涨期权。每个类可能继承自更高层级类的属性和方法。

聚合(Aggregation)#

聚合是指一个对象至少部分由多个可能独立存在的其他对象组成的情况。建模欧式看涨期权的类可能将标的股票和相关贴现短期利率的其他对象作为属性。表示股票和短期利率的对象也可以独立地被其他对象使用。

组合(Composition)#

组合类似于聚合,但这里的单个对象不能彼此独立存在。考虑一个定制化的利率互换,包含固定端和浮动端。这两端不能独立于互换本身存在。

多态(Polymorphism)#

多态可以有多种形式。在 Python 语境中,特别重要的是所谓的鸭子类型(Duck Typing)。这指的是标准操作可以在许多不同的类及其实例上实现,而不必确切知道处理的是什么对象。对于金融工具类,这可能意味着可以调用 get_current_price() 方法,而与对象的具体类型(股票、期权、互换)无关。

封装(Encapsulation)#

这个概念指的是使类中的数据只能通过公共方法访问的方法。建模股票的类可能有一个属性 current_stock_price。封装将通过方法 get_current_stock_price() 提供对属性值的访问,并将数据对用户隐藏(即使其私有)。这种方法可能避免简单地操作和可能更改属性值带来的意外影响。然而,在 Python 类中使数据私有的方式是有局限的。

在更高层面上,这些方面中的许多可以归纳为软件工程的两个总体目标:

可复用性(Reusability)#

继承和多态等概念提高了代码的可复用性,提高了程序员的效率和生产能力。它们还简化了代码维护。

无冗余性(Nonredundancy)#

同时,这些方法允许构建几乎无冗余的代码,避免重复的实现工作,并减少调试和测试工作以及维护工作量。它们还可能带来更小的整体代码库。

本章组织如下:

"Python对象概览" on page 149

本节通过 OOP 的视角审视一些 Python 对象。

"Python类基础" on page 154

本节介绍 Python 中 OOP 的核心元素,并以金融工具和投资组合头寸作为主要示例。

"Python数据模型" on page 159

本节讨论 Python 数据模型的重要元素以及某些特殊方法的作用。

Python对象概览#

让我们先从 OOP 程序员的视角,简要回顾一下在前几章中遇到的一些标准对象。

int#

从一个简单的例子开始,考虑一个整数对象。即使对于这样一个简单的 Python 对象,主要的 OOP 特性也是存在的:

In [7]: n = 5
In [8]: type(n)
Out[8]: int
In [9]: n.numerator
Out[9]: 5
In [10]: n.bit_length()
Out[10]: 3
In [11]: n + n
Out[11]: 10
In [12]: 2 * n
Out[12]: 10
In [13]: n.__sizeof__()
Out[13]: 28
① 新实例 n。

对象的类型。

一个属性。

一个方法。

⑤ 应用 + 运算符(加法)。

应用 * 运算符(乘法)。

调用特殊方法 __sizeof__() 获取以字节为单位的内存使用量。1

list#

列表对象有更多方法,但基本行为相同:

In [14]: l = [1, 2, 3, 4] ①
In [15]: type(l) ②
Out[15]: list
In [16]: l[0] ③
Out[16]: 1
In [17]: l.append(10) ④
In [18]: l + l ⑤
Out[18]: [1, 2, 3, 4, 10, 1, 2, 3, 4, 10]
In [19]: 2 * l ⑥
Out[19]: [1, 2, 3, 4, 10, 1, 2, 3, 4, 10]
In [20]: sum(l) ⑦
Out[20]: 20
In [21]: l.__sizeof__() ⑧
Out[21]: 104

新实例 l。

对象的类型。

通过索引选择一个元素。

一个方法。

应用 + 运算符(连接)。

应用 * 运算符(连接)。

应用标准 Python 函数 sum()。

调用特殊方法 __sizeof__() 获取以字节为单位的内存使用量。

ndarray#

int 和 list 对象是标准 Python 对象。NumPy 的 ndarray 对象是一个来自开源包的"定制"对象:

In [22]: import numpy as np ①
In [23]: a = np.arange(16).reshape((4, 4)) ②
In [24]: a ②
Out[24]: array([[0, 1, 2, 3],
    [4, 5, 6, 7],
    [8, 9, 10, 11],
    [12, 13, 14, 15]])
In [25]: type(a) ③
Out[25]: numpy.ndarray

导入 numpy。

新实例 a。

对象的类型。

虽然 ndarray 对象不是标准对象,但它在许多情况下的行为就像标准对象一样——这得益于 Python 数据模型,将在本章后面解释:

In [26]: a.nbytes ①
Out[26]: 128

In [27]: a.sum() ②
Out[27]: 120

In [28]: a.cumsum(axis=0) ③
Out[28]: array([[0, 1, 2, 3], [4, 6, 8, 10], [12, 15, 18, 21], [24, 28, 32, 36]])
In [29]: a + a ④
Out[29]: array([[0, 2, 4, 6],
[8, 10, 12, 14],
[16, 18, 20, 22],
[24, 26, 28, 30]])

In [30]: 2 * a
Out[30]: array([[0, 2, 4, 6],
[8, 10, 12, 14],
[16, 18, 20, 22],
[24, 26, 28, 30]])
In [31]: sum(a)
Out[31]: array([24, 28, 32, 36])
In [32]: np.sum(a)
Out[32]: 120

In [33]: a.__sizeof__()
Out[33]: 112

一个属性。

一个方法(聚合)。

一个方法(非聚合)。

应用 + 运算符(加法)。

应用 * 运算符(乘法)。

应用标准 Python 函数 sum()。

应用 NumPy 通用函数 np.sum()。

调用特殊方法 __sizeof__() 获取以字节为单位的内存使用量。

DataFrame#

最后,快速看一下 pandas 的 DataFrame 对象,它的行为与 ndarray 对象类似。首先,基于 ndarray 对象实例化 DataFrame 对象:

In [34]: import pandas as pd ①
In [35]: df = pd.DataFrame(a, columns=list('abcd')) ②
In [36]: type(df) ③
Out[36]: pandas.core.frame.DataFrame

导入 pandas。

新实例 df。

对象的类型。

其次,查看属性、方法和操作:

In [37]: df.columns ①
Out[37]: Index(['a', 'b', 'c', 'd'], dtype='object')

In [38]: df.sum() ②
Out[38]: a 24
    b 28
    c 32
    d 36
    dtype: int64

In [39]: df.cumsum() ③
Out[39]: a b c d
00123146810212151821324283236

In [40]: df + df ④
Out[40]: a b c d
0024618101214216182022324262830

In [41]: 2 * df ⑤
Out[41]: a b c d
0024618101214216182022324262830

In [42]: np.sum(df) ⑥
Out[42]: a 24
    b 28
    c 32
    d 36
    dtype: int64

In [43]: df.__sizeof__() ⑦
Out[43]: 208

一个属性。#

一个方法(聚合)。

一个方法(非聚合)。

应用 + 运算符(加法)。

应用 * 运算符(乘法)。

应用 NumPy 通用函数 np.sum()。

调用特殊方法 __sizeof__() 获取以字节为单位的内存使用量。

Python类基础#

本节介绍在 Python 中使用 OOP 的主要概念和具体语法。此处的背景是构建自定义类,以建模那些无法通过现有 Python 对象类型轻松、高效或恰当地建模的对象类型。整个过程中,使用金融工具作为示例。

两行代码就足以创建一个新的 Python 类:

In [44]: class FinancialInstrument(object): ①
    pass ②

In [45]: fi = FinancialInstrument() ③

In [46]: type(fi) ④
Out[46]: __main__.FinancialInstrument

In [47]: fi ⑤
Out[47]: <__main__.FinancialInstrument at 0x116767278>

In [48]: fi.__str__() ⑤
Out[48]: '<__main__.FinancialInstrument object at 0x116767278>'
In [49]: fi.price = 100 ⑥

In [50]: fi.price ⑥
Out[50]: 100

类定义语句。2

一些代码;这里只是 pass 关键字。

类的一个新实例,名为 aapl。

对象的类型。

每个 Python 对象都带有某些"特殊"属性和方法(来自 object);这里调用了特殊方法以获取字符串表示。

所谓的数据属性(Data Attributes)——与常规属性相对——可以在运行时为每个对象动态定义。

一个重要的特殊方法是 __init__,它在每次实例化对象时被调用。它接收对象本身(按照惯例为 self)和可能的其他参数:

In [51]: class FinancialInstrument(object):
    author = 'Yves Hilpisch'
    def__init__(self, symbol, price):
    self.symbol = symbol
    self.price = price

In [52]: FinancialInstrument.author
Out[52]: 'Yves Hilpisch'

In [53]: aapl = FinancialInstrument('AAPL', 100)

In [54]: aapl.symbol
Out[54]: 'AAPL'

In [55]: aapl.author
Out[55]: 'Yves Hilpisch'

In [56]: aapl.price = 105

In [57]: aapl.price
Out[57]: 105

类属性定义(每个实例继承)。

初始化期间调用的特殊方法 __init__。

实例属性定义(每个实例独立)。

类的新实例,名为 fi。

访问实例属性。

访问类属性。

更改实例属性的值。#

金融工具的价格会定期变化,但金融工具的代码可能不会变化。为了向类定义引入封装,可以定义两个方法:get_price() 和 set_price()。下面的代码还继承了之前的类定义(不再继承自 object):

In [58]: class FinancialInstrument(FinancialInstrument): ①
    def get_price(self): ②
    return self.price ②
    def set_price(self, price): ③
    self.price = price ④

In [59]: fi = FinancialInstrument('AAPL', 100) ⑤

In [60]: fi.get_price() ⑥
Out[60]: 100

In [61]: fi.set_price(105) ⑦

In [62]: fi.get_price() ⑥
Out[62]: 105

In [63]: fi.price ⑧
Out[63]: 105

通过继承前一版本的类定义。

定义 get_price() 方法。

定义 set_price() 方法……

……并根据参数值更新实例属性值。

基于新类定义的新实例,名为 fi。

调用 get_price() 方法读取实例属性值。

通过 set_price() 更新实例属性值。

直接访问实例属性。

封装通常的目标是向使用类的用户隐藏数据。添加 getter 和 setter 方法是为实现这一目标所做的一部分工作。然而,这并不能阻止用户直接访问和操作实例属性。这就是私有实例属性发挥作用的地方。它们由两个前导下划线定义:

In [67]: fi.__price
AttributeError Traceback (most recent call last)
<ipython-input-67-bd62f6cadb79> in <module>
----> 1 fi.__price
AttributeError: 'FinancialInstrument' object has no attribute '__price'
In [64]: class FinancialInstrument(object):
    def__init__(self, symbol, price):
    self.symbol = symbol
    self.__price = price ①
    def get_price(self):
    return self.__price
    def set_price(self, price):
    self.__price = price
In [65]: fi = FinancialInstrument('AAPL', 100)
In [66]: fi.get_price() 2
Out[66]: 100
In [68]: fi._FinancialInstrument__price 4
Out[68]: 100
In [69]: fi._FinancialInstrument__price = 105
In [70]: fi.set_price(100) 5

price 被定义为私有实例属性。

方法 get_price() 返回其值。

尝试直接访问属性会引发错误。

如果加上类名并加一个前导下划线,仍然可以直接访问和操作。

将价格设置回原始值。

Python中的封装#

虽然可以通过私有实例属性和相应的方法在 Python 类中实现封装,但无法完全强制向用户隐藏数据。在这个意义上,在 Python 中,封装更像是一种工程原则,而不是 Python 类的技术特性。

考虑另一个建模金融工具投资组合头寸的类。通过这两个类,聚合的概念很容易说明。PortfolioPosition 类的实例将一个 FinancialInstrument 类的实例作为属性值。添加一个实例属性,如 position_size,就可以计算例如头寸价值:

In [71]: class PortfolioPosition(object):
    def__init__(self, financial_instrument, position_size):
    self.position = financial_instrument ①
    self.__position_size = position_size ②
    def get_position_size(self):
    return self.__position_size
    def update_position_size(self, position_size):
    self.__position_size = position_size
    def get_position_value(self):
    return self.__position_size * \
    self.position.get_price() ③

In [72]: pp = PortfolioPosition(fi, 10)

In [73]: pp.get_position_size()
Out[73]: 10

In [74]: pp.get_position_value() ③
Out[74]: 1000

In [75]: pp.position.get_price() ④
Out[75]: 100

In [76]: pp.position.set_price(105) ⑤

In [77]: pp.get_position_value() ⑥
Out[77]: 1050

基于 FinancialInstrument 类实例的实例属性。

PortfolioPosition 类的私有实例属性。

基于属性计算头寸价值。

可以直接访问附加到实例属性对象的方法(也可以隐藏)。

更新金融工具的价格。

基于更新后的价格计算新的头寸价值。

Python数据模型#

前一节的示例突出了所谓的 Python 数据模型(Data Model)或对象模型的一些方面。Python 数据模型允许设计出与 Python 基本语言结构一致交互的类。除其他外,它支持(参见 Ramalho (2015),第4页)以下任务和结构:

• 迭代(Iteration)

• 集合处理(Collection Handling)

• 属性访问(Attribute Access)

• 运算符重载(Operator Overloading)

• 函数和方法调用(Function and Method Invocation)

• 对象的创建和销毁(Object Creation and Destruction)

• 字符串表示(例如用于打印)

• 管理上下文(Managed Contexts,即 with 块)

由于 Python 数据模型如此重要,本节专门通过一个示例(来自 Ramalho (2015),稍作调整)来探讨其若干方面。它实现了一个一维三元素向量的类(想想欧几里得空间中的向量)。首先,特殊方法 __init__:

In [78]: class Vector(object):
    def__init__(self, x=0, y=0, z=0): ①
    self.x = x ①
    self.y = y ①
    self.z = z ①
In [79]: v = Vector(1, 2, 3)
In [80]: v ③
Out[80]: <__main__.Vector at 0x1167789e8>

三个预初始化的实例属性(想象三维空间)。

类的新实例,名为 v。

默认的字符串表示。

特殊方法 __repr__ 允许定义自定义的字符串表示:

In [81]: class Vector(Vector):
    def__repr__(self):
    return 'Vector(%r, %r, %r)' % (self.x, self.y, self.z)

In [82]: v = Vector(1, 2, 3)

In [83]: v
Out[83]: Vector(1, 2, 3)

In [84]: print(v)
    Vector(1, 2, 3)

新的字符串表示。

abs() 和 bool() 是两个标准 Python 函数,其在 Vector 类上的行为可以通过特殊方法 __abs__ 和 __bool__ 来定义:

In [85]: class Vector(Vector):
    def__abs__(self):
    return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5
def__bool__(self):
    return bool(abs(self))

In [86]: v = Vector(1, 2, -1)
In [87]: abs(v)
Out[87]: 2.449489742783178

In [88]: bool(v)
Out[88]: True

In [89]: v = Vector()
In [90]: v
Out[90]: Vector(0, 0, 0)

In [91]: abs(v)
Out[91]: 0.0

In [92]: bool(v)
Out[92]: False

根据三个属性值返回欧几里得范数。

属性值非零的新 Vector 对象。

属性值全为零的新 Vector 对象。#

如前多次所示,+ 和 * 运算符可以应用于几乎任何 Python 对象。其行为通过特殊方法 __add__ 和 __mul__ 定义:

In [93]: class Vector(Vector):
    def__add__(self, other):
    x = self.x + other.x
    y = self.y + other.y
    z = self.z + other.z
    return Vector(x, y, z)
def__mul__(self, scalar):
    return Vector(self.x * scalar,
    self.y * scalar,
    self.z * scalar)
In [94]: v = Vector(1, 2, 3)
In [95]: v + Vector(2, 3, 4)
Out[95]: Vector(3, 5, 7)
In [96]: v * 2
Out[96]: Vector(2, 4, 6)

在这种情况下,每个特殊方法返回其自身类型的对象。#

另一个标准 Python 函数是 len(),它返回对象中元素的数量。该函数在对象上调用时访问特殊方法 __len__。另一方面,特殊方法 __getitem__ 使通过方括号表示法的索引成为可能:

In [97]: class Vector(Vector):
    def__len__(self):
    return 3

    def__getitem__(self, i):
    if i in [0, -3]: return self.x
    elif i in [1, -2]: return self.y
    elif i in [2, -1]: return self.z
    else: raise IndexError('Index out of range.')
In [98]: v = Vector(1, 2, 3)

In [99]: len(v)
Out[99]: 3

In [100]: v[0]
Out[100]: 1

In [101]: v[-2]
Out[101]: 2
In [102]: v[3]
INDEXError Traceback (most recent call last)
<ipython-input-102-f998c57dcc1e> in <module>
----> 1 v[3]
<ipython-input-97-b0ca25eef7b3> in__getitem__(self, i)
    7    elif i in [1, -2]: return self.y
    8    elif i in [2, -1]: return self.z
----> 9    else: raise IndexError('Index out of range.')
INDEXError: Index out of range.

Vector 类的所有实例的长度都是三。#

最后,特殊方法 __iter__ 定义了在迭代对象元素时的行为。定义了此操作的对象称为可迭代对象(Iterable)。例如,所有集合和容器都是可迭代的:

In [103]: class Vector(Vector):
    def__iter__(self):
    for i in range(len(self)):
    yield self[i]

In [104]: v = Vector(1, 2, 3)

In [105]: for i in range(3): ①
    print(v[i]) ①
    123

In [106]: for coordinate in v: ②
    print(coordinate) ②
    123

使用索引值的间接迭代(通过 __getitem__)。

直接迭代类实例(使用 __iter__)。

Python 数据模型允许定义与标准 Python 运算符、函数等无缝交互的 Python 类。这使得 Python 成为一种相当灵活的编程语言,可以轻松地通过新的类和对象类型进行扩展。

作为总结,下面在一个代码块中提供 Vector 类的完整定义。

Vector类#

In [107]: class Vector(object):
    def__init__(self, x=0, y=0, z=0):
    self.x = x
    self.y = y
    self.z = z

    def__repr__(self):
    return 'Vector(%r, %r, %r)' % (self.x, self.y, self.z)

    def__abs__(self):
    return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5

    def__bool__(self):
    return bool(abs(self))

    def__add__(self, other):
    x = self.x + other.x
    y = self.y + other.y
    z = self.z + other.z
    return Vector(x, y, z)

    def__mul__(self, scalar):
    return Vector(self.x * scalar,
    self.y * scalar,
    self.z * scalar)

    def__len__(self):
    return 3

    def__getitem__(self, i):
    if i in [0, -3]: return self.x
    elif i in [1, -2]: return self.y
    elif i in [2, -1]: return self.z
    else: raise IndexError('Index out of range.')
def__iter__(self):
    for i in range(len(self)):
    yield self[i]

结语#

本章从理论和 Python 示例两个角度介绍了面向对象编程的概念和方法。OOP 是 Python 中使用的主要编程范式之一。它不仅允许建模和实现相当复杂的应用程序,而且还允许创建自定义对象,由于灵活的 Python 数据模型,这些对象的行为类似于标准 Python 对象。尽管有许多批评者反对 OOP,但可以肯定地说,它为 Python 程序员和宽客提供了强大的工具,在达到一定复杂性时非常有帮助。第五部分开发和讨论的衍生品定价包就是这样一个案例,其中 OOP 似乎是处理固有复杂性和抽象要求的唯一明智的编程范式。

延伸资源#

以下是关于一般 OOP 以及特别是 Python 编程和 OOP 的宝贵在线资源:

• Lecture Notes on Object-Oriented Programming

• Object-Oriented Programming in Python

关于 Python 面向对象和 Python 数据模型的优秀书籍资源是:

• Ramalho, Luciano (2016). Fluent Python. Sebastopol, CA: O'Reilly.

金融数据科学#

本部分介绍金融数据科学的基本技术、方法和包。许多主题(如可视化)和许多包(如 scikit-learn)对于使用 Python 进行数据科学至关重要。从这个意义上说,本部分为宽客和金融分析师提供了成为金融数据科学家所需的 Python 工具。

与第二部分类似,各章按主题组织,因此每章都可以作为感兴趣主题的参考资料:

• 第7章讨论使用 matplotlib 和 plotly 进行静态和交互式可视化。

• 第8章介绍使用 pandas 处理金融时间序列数据。

• 第9章重点介绍如何正确且快速地进行输入/输出(I/O)操作。

• 第10章全是关于如何使 Python 代码更快的。

• 第11章关注金融中经常需要的数学工具。

• 第12章介绍使用 Python 实现随机方法。

• 第13章介绍统计和机器学习方法。