6. OOP I:对象与方法#

6.1. 概述#

传统的编程范式(如 Fortran、C、MATLAB 等)被称为过程式编程

其工作方式如下:

  • 程序具有一个状态,对应于其变量的值。

  • 调用函数来对状态进行操作和转换。

  • 最终输出通过一系列函数调用产生。

另外两种重要的编程范式是面向对象编程(OOP)和函数式编程

在面向对象编程范式中,数据和函数被捆绑在一起形成”对象”——在这种情境下,函数被称为方法

方法被调用来转换对象中包含的数据。

  • 可以想象一个 Python 列表,它包含数据,并具有诸如 append()pop() 这样用于转换数据的方法。

函数式编程语言建立在函数组合的思想之上。

那么 Python 属于哪一类呢?

实际上,Python 是一种实用主义语言,它融合了面向对象、函数式和过程式风格,而非采取纯粹主义的方法。

一方面,这使得 Python 及其用户能够有选择地借鉴不同范式的优点。

另一方面,这种不纯粹性有时可能会导致一些混淆。

幸运的是,如果你理解 Python 在基础层面上面向对象的,这种混淆就会降到最低。

我们的意思是,在 Python 中,一切皆对象

在本讲座中,我们将解释这句话的含义以及它为何重要。

我们将使用以下第三方库:

!pip install rich

Hide code cell output

Requirement already satisfied: rich in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (14.2.0)
Requirement already satisfied: markdown-it-py>=2.2.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from rich) (3.0.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from rich) (2.19.2)
Requirement already satisfied: mdurl~=0.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from markdown-it-py>=2.2.0->rich) (0.1.2)

6.2. 对象#

在 Python 中,对象是存储在计算机内存中的数据和指令的集合,包含:

  1. 类型

  2. 唯一标识符

  3. 数据(即内容)

  4. 方法

以下将依次定义和讨论这些概念。

6.2.1. 类型#

Python 提供了不同类型的对象,以适应不同类别的数据。

例如:

s = 'This is a string'
type(s)
str
x = 42   # Now let's create an integer
type(x)
int

对象的类型对许多表达式都很重要。

例如,两个字符串之间的加法运算符表示连接:

'300' + 'cc'
'300cc'

而两个数字之间则表示普通加法:

300 + 400
700

考虑以下表达式:

'300' + 400
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 '300' + 400

TypeError: can only concatenate str (not "int") to str

这里我们混合了类型,Python 不清楚用户是想要

  • '300' 转换为整数,然后将其与 400 相加,还是

  • 400 转换为字符串,然后将其与 '300' 连接

某些语言可能会尝试猜测,但 Python 是强类型的:

  • 类型很重要,隐式类型转换很少发生。

  • Python 会通过引发 TypeError 来响应。

为了避免错误,你需要通过更改相关类型来明确你的意图。

例如:

int('300') + 400   # To add as numbers, change the string to an integer
700

6.2.2. 标识符#

在 Python 中,每个对象都有一个唯一标识符,帮助 Python(和我们)跟踪该对象。

可以通过 id() 函数获取对象的标识符:

y = 2.5
z = 2.5
id(y)
139957202295376
id(z)
139957147607472

在这个例子中,yz 恰好具有相同的值(即 2.5),但它们不是同一个对象。

对象的标识符实际上就是该对象在内存中的地址。

6.2.3. 对象内容:数据与属性#

如果我们设置 x = 42,那么我们创建了一个包含数据 42int 类型对象。

实际上,它包含的内容更多,如以下示例所示:

x = 42
x
42
x.imag
0
x.__class__
int

当 Python 创建这个整数对象时,它会随之存储各种辅助信息,例如虚部和类型。

点号后面的任何名称都称为点号左侧对象的属性

  • 例如,imag__class__x 的属性。

从这个例子中我们可以看到,对象具有包含辅助信息的属性。

它们还具有像函数一样起作用的属性,称为方法

这些属性非常重要,让我们深入讨论它们。

6.2.4. 方法#

方法是与对象捆绑在一起的函数

形式上,方法是对象的可调用属性——即可以作为函数调用的属性:

x = ['foo', 'bar']
callable(x.append)
True
callable(x.__doc__)
False

方法通常作用于它们所属对象中包含的数据,或将该数据与其他数据结合:

x = ['a', 'b']
x.append('c')
s = 'This is a string'
s.upper()
'THIS IS A STRING'
s.lower()
'this is a string'
s.replace('This', 'That')
'That is a string'

Python 的大量功能都是围绕方法调用组织的。

例如,考虑以下代码:

x = ['a', 'b']
x[0] = 'aa'  # Item assignment using square bracket notation
x
['aa', 'b']

看起来这里没有使用任何方法,但实际上方括号赋值符号只是方法调用的一个便捷接口。

实际发生的是 Python 调用了 __setitem__ 方法,如下所示:

x = ['a', 'b']
x.__setitem__(0, 'aa')  # Equivalent to x[0] = 'aa'
x
['aa', 'b']

(如果你愿意,你可以修改 __setitem__ 方法,使方括号赋值做一些完全不同的事情)

6.3. 使用 Rich 进行检查#

有一个名为 rich 的好用包,可以帮助我们查看对象的内容。

例如:

from rich import inspect
x = 10
inspect(10)
╭────── <class 'int'> ───────╮
 int([x]) -> integer        
 int(x, base=10) -> integer 
                            
 ╭────────────────────────╮ 
  10                      
 ╰────────────────────────╯ 
                            
 denominator = 1            
        imag = 0            
   numerator = 10           
        real = 10           
╰────────────────────────────╯

如果我们也想查看方法,可以使用:

inspect(10, methods=True)
╭───────────────────────────────────────────────── <class 'int'> ─────────────────────────────────────────────────╮
 int([x]) -> integer                                                                                             
 int(x, base=10) -> integer                                                                                      
                                                                                                                 
 ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 
  10                                                                                                           
 ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 
                                                                                                                 
      denominator = 1                                                                                            
             imag = 0                                                                                            
        numerator = 10                                                                                           
             real = 10                                                                                           
 as_integer_ratio = def as_integer_ratio(): Return a pair of integers, whose ratio is equal to the original int. 
        bit_count = def bit_count(): Number of ones in the binary representation of the absolute value of self.  
       bit_length = def bit_length(): Number of bits necessary to represent self in binary.                      
        conjugate = def conjugate(): Returns self, the complex conjugate of any int.                             
       from_bytes = def from_bytes(bytes, byteorder='big', *, signed=False): Return the integer represented by   
                    the given array of bytes.                                                                    
       is_integer = def is_integer(): Returns True. Exists for duck type compatibility with float.is_integer.    
         to_bytes = def to_bytes(length=1, byteorder='big', *, signed=False): Return an array of bytes           
                    representing an integer.                                                                     
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

实际上还有更多方法,如果你执行 inspect(10, all=True) 可以看到。

6.4. 一个小谜题#

在本讲座中,我们声称 Python 在本质上是一种面向对象的语言。

但以下是一个看起来更像过程式的例子:

x = ['a', 'b']
m = len(x)
m
2

如果 Python 是面向对象的,为什么我们不使用 x.len() 呢?

答案与 Python 追求可读性和一致风格这一事实有关。

在 Python 中,用户构建自定义对象是很常见的——我们将在后面讨论如何做到这一点。

用户很常见地会向他们的对象添加方法来测量对象的长度(根据适当的定义)。

在命名这样的方法时,自然的选择是 len()length()

如果一些用户选择 len(),而另一些用户选择 length(),那么风格将不一致,更难记忆。

为了避免这种情况,Python 的创建者选择将 len() 作为内置函数,以强调 len() 是约定俗成的用法。

话虽如此,Python 在底层仍然是面向对象的。

实际上,上面讨论的列表 x 有一个名为 __len__() 的方法。

len() 函数所做的就是调用这个方法。

换句话说,以下代码是等价的:

x = ['a', 'b']
len(x)
2

x = ['a', 'b']
x.__len__()
2

6.5. 总结#

本讲座的核心信息很明确:

  • 在 Python 中,内存中的一切都被视为对象

这不仅包括列表、字符串等,还包括一些不那么明显的东西,例如:

  • 函数(一旦被读入内存)

  • 模块(同上)

  • 打开用于读取或写入的文件

  • 整数等

记住一切皆对象将帮助你与程序交互并编写清晰的 Python 风格代码。

6.6. 练习#

Exercise 6.1

我们之前已经接触过布尔数据类型

利用本讲座中学到的知识,打印布尔对象 True 的方法列表。