8.4 KiB
doc_type, full_text
| doc_type | full_text |
|---|---|
| short | sources/03_Special_methods.md |
03_Special_methods 总结
本文介绍 Python 类中用于定制对象行为的“特殊方法”(special methods / magic methods),并说明字符串表示、运算符重载、容器协议、方法调用过程、绑定方法以及动态属性访问的基本机制。相关主题可连接到 Python特殊方法、对象表示、concepts/绑定方法、concepts/动态属性访问。
核心主题
特殊方法的作用
Python 类可以定义以双下划线开头和结尾的方法,例如 __init__、__repr__。这些方法对解释器具有特殊意义,用于让用户自定义对象在内置语法、函数和操作符中的行为。
示例:
class Stock(object):
def __init__(self):
...
def __repr__(self):
...
本文强调:Python 中许多“看似内置”的行为,实际上会转化为对对象特殊方法的调用。这是 Python数据模型 的重要组成部分。
字符串转换相关特殊方法
对象通常有两种字符串表示:
str(obj):面向用户的、适合打印的友好表示。repr(obj):面向程序员的、更精确或更可复现的表示。
例如 datetime.date 对象:
>>> print(d)
2012-12-21
>>> d
datetime.date(2012, 12, 21)
类可以通过以下方法控制这两种表示:
class Date(object):
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __str__(self):
return f'{self.year}-{self.month}-{self.day}'
def __repr__(self):
return f'Date({self.year},{self.month},{self.day})'
__repr__() 的约定
__repr__() 通常应返回一个字符串,使其在可能的情况下能通过 eval() 重建原对象。例如:
Date(2012,12,21)
如果无法做到可重建,则应返回清晰、便于调试的表示。这与 对象表示 和 调试友好代码 密切相关。
数学运算相关特殊方法
Python 的数学运算符会转换为对象上的特殊方法调用。例如:
a + b a.__add__(b)
a - b a.__sub__(b)
a * b a.__mul__(b)
a / b a.__truediv__(b)
a // b a.__floordiv__(b)
a % b a.__mod__(b)
a ** b a.__pow__(b)
-a a.__neg__()
abs(a) a.__abs__()
这说明类可以通过实现这些方法来自定义数学行为,也就是常说的运算符重载。相关概念可整理为 运算符重载。
容器访问相关特殊方法
为了让自定义对象表现得像序列、列表、字典或其他容器,可以实现以下特殊方法:
len(x) x.__len__()
x[a] x.__getitem__(a)
x[a] = v x.__setitem__(a,v)
del x[a] x.__delitem__(a)
示例结构:
class Sequence:
def __len__(self):
...
def __getitem__(self,a):
...
def __setitem__(self,a,v):
...
def __delitem__(self,a):
...
这体现了 Python 的协议式设计:对象不需要继承某个特定基类,只要实现相应方法,就能参与对应语法。这与 Python协议 和 容器协议 相关。
方法调用的两步过程
调用方法实际上分为两步:
- 属性查找:使用
.操作符取得方法对象。 - 函数调用:使用
()调用该方法。
示例:
>>> s = Stock('GOOG',100,490.10)
>>> c = s.cost # 查找
>>> c
<bound method Stock.cost of <Stock object at 0x590d0>>
>>> c() # 调用
49010.0
这解释了为什么 s.cost 和 s.cost() 是不同的:前者只是取得一个方法对象,后者才真正执行方法。
绑定方法
尚未被 () 调用的方法对象称为绑定方法(bound method)。它已经绑定到某个具体实例,因此之后调用时会自动作用于该实例。
>>> s = Stock('GOOG', 100, 490.10)
>>> c = s.cost
>>> c()
49010.0
绑定方法常导致隐蔽错误,尤其是忘记加括号时:
print('Cost : %0.2f' % s.cost)
这里传入的是方法对象,而不是方法返回值,因此会触发类型错误。
另一个典型错误:
f = open(filename, 'w')
f.close # 没有真正关闭文件
正确写法应为:
f.close()
这一节强调:在 Python 中,方法也是对象;访问方法和调用方法是两个不同动作。相关概念可连接到 concepts/绑定方法、一等对象。
动态属性访问
除了使用点号语法访问属性,Python 还提供一组内置函数用于动态操作属性:
getattr(obj, 'name') # 等同于 obj.name
setattr(obj, 'name', value) # 等同于 obj.name = value
delattr(obj, 'name') # 等同于 del obj.name
hasattr(obj, 'name') # 判断属性是否存在
示例:
if hasattr(obj, 'x'):
x = getattr(obj, 'x')
else:
x = None
getattr() 还可以提供默认值:
x = getattr(obj, 'x', None)
动态属性访问允许程序根据字符串形式的字段名获取对象属性,是构建通用工具、表格打印器、序列化器等代码的重要基础。相关主题包括 concepts/动态属性访问、反射、通用编程。
练习 4.9:改进对象打印输出
练习要求修改 stock.py 中的 Stock 类,使 __repr__() 返回更有用的表示,例如:
>>> goog = Stock('GOOG', 100, 490.1)
>>> goog
Stock('GOOG', 100, 490.1)
然后观察当读取投资组合并查看列表时,输出会发生什么变化:
>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> portfolio
该练习的重点是:列表在显示元素时会使用元素的 repr(),因此定义良好的 __repr__() 会显著改善调试和交互式查看体验。
练习 4.10:使用 getattr() 构建通用表格打印函数
练习展示了 getattr() 的灵活性:
>>> import stock
>>> s = stock.Stock('GOOG', 100, 490.1)
>>> columns = ['name', 'shares']
>>> for colname in columns:
print(colname, '=', getattr(s, colname))
name = GOOG
shares = 100
输出完全由 columns 中列出的属性名决定。练习要求在 tableformat.py 中将这个思想扩展为通用函数 print_table():
- 输入任意对象列表。
- 输入用户指定的属性名列表。
- 输入
TableFormatter实例控制输出格式。
目标用法:
>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> from tableformat import create_formatter, print_table
>>> formatter = create_formatter('txt')
>>> print_table(portfolio, ['name','shares'], formatter)
进一步也可以打印更多列:
>>> print_table(portfolio, ['name','shares','price'], formatter)
该练习把动态属性访问和格式化输出结合起来,是通用报表生成的基础,也与 表格格式化、对象属性驱动设计 相关。
关键结论
- Python 的许多语言特性由特殊方法驱动。
__str__()控制用户友好的字符串表示,__repr__()控制程序员友好的表示。- 数学运算符和容器操作都会映射到相应特殊方法。
- 方法调用分为“查找”和“调用”两步,忘记
()会得到绑定方法而非执行结果。 getattr()、setattr()、delattr()、hasattr()支持基于字符串的动态属性访问。- 动态属性访问能让函数处理任意对象和任意字段,是构建灵活工具的重要技巧。
可沉淀的概念页
- Python特殊方法:整理双下划线方法如何定制对象行为。
- 对象表示:比较
str()、repr()、__str__()、__repr__()的用途。 - 运算符重载:说明数学符号如何映射到特殊方法。
- 容器协议:总结
__len__()、__getitem__()等容器相关方法。 - concepts/绑定方法:解释方法查找、实例绑定和调用之间的区别。
- concepts/动态属性访问:总结
getattr()等函数在通用编程中的用途。 - Python协议:归纳 Python 中“实现方法即实现协议”的设计思想。