6. OOP I:对象与方法#
6.1. 概述#
传统的编程范式(如 Fortran、C、MATLAB 等)被称为过程式编程。
其工作方式如下:
程序具有一个状态,对应于其变量的值。
调用函数来对状态进行操作和转换。
最终输出通过一系列函数调用产生。
另外两种重要的编程范式是面向对象编程(OOP)和函数式编程。
在面向对象编程范式中,数据和函数被捆绑在一起形成”对象”——在这种情境下,函数被称为方法。
方法被调用来转换对象中包含的数据。
可以想象一个 Python 列表,它包含数据,并具有诸如
append()和pop()这样用于转换数据的方法。
函数式编程语言建立在函数组合的思想之上。
那么 Python 属于哪一类呢?
实际上,Python 是一种实用主义语言,它融合了面向对象、函数式和过程式风格,而非采取纯粹主义的方法。
一方面,这使得 Python 及其用户能够有选择地借鉴不同范式的优点。
另一方面,这种不纯粹性有时可能会导致一些混淆。
幸运的是,如果你理解 Python 在基础层面上是面向对象的,这种混淆就会降到最低。
我们的意思是,在 Python 中,一切皆对象。
在本讲座中,我们将解释这句话的含义以及它为何重要。
我们将使用以下第三方库:
!pip install rich
6.2. 对象#
在 Python 中,对象是存储在计算机内存中的数据和指令的集合,包含:
类型
唯一标识符
数据(即内容)
方法
以下将依次定义和讨论这些概念。
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
在这个例子中,y 和 z 恰好具有相同的值(即 2.5),但它们不是同一个对象。
对象的标识符实际上就是该对象在内存中的地址。
6.2.3. 对象内容:数据与属性#
如果我们设置 x = 42,那么我们创建了一个包含数据 42 的 int 类型对象。
实际上,它包含的内容更多,如以下示例所示:
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 的方法列表。
Hint
你可以使用 callable() 来测试对象的某个属性是否可以作为函数调用。
Solution
首先,我们需要找到 True 的所有属性,可以通过以下方式完成:
print(sorted(True.__dir__()))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
或者:
print(sorted(dir(True)))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
由于布尔数据类型是一种原始类型,你也可以在内置命名空间中找到它:
print(dir(__builtins__.bool))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
这里我们使用 for 循环来筛选出可调用的属性:
attributes = dir(__builtins__.bool)
callablels = []
for attribute in attributes:
# 使用 eval() 将字符串作为表达式求值
if callable(eval(f'True.{attribute}')):
callablels.append(attribute)
print(callablels)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'from_bytes', 'is_integer', 'to_bytes']