类(一):对象与封装
Class I: Objects and Encapsulation
用类组织相关的数据与操作,分析初始化、实例方法和接收者绑定。检查对象状态的有效性,区分类属性、实例属性与共享引用,并理解类方法及非公开接口的边界。
本讲结束后你应当能
- 区分类与实例,用属性保存每个实例的状态
- 解释初始化、默认参数及普通实例方法中的 self
- 分别核对方法的返回值、输出与状态变化
- 在构造及更新入口验证数据,解释下划线约定的限制
- 区分实例属性、类属性、共享集合及类方法
- 根据状态、接口与生命周期选择类、函数或记录
本页目录
I. 对象与初始化
数据与相关操作
上一讲从规格、接口、边界和副作用检查程序。本讲进一步研究怎样组织这些接口和数据。
平面点需要保存坐标,可以显示位置、平移或计算到另一点的距离。使用字典与普通函数已经能够完成任务:
def make_point(x: float, y: float) -> dict[str, float]:
return {"x": x, "y": y}
def distance(p: dict[str, float], q: dict[str, float]) -> float:
dx = p["x"] - q["x"]
dy = p["y"] - q["y"]
return (dx * dx + dy * dy) ** 0.5
a = make_point(0, 0)
b = make_point(3, 4)
print(distance(a, b))
结果为 5.0。这个实现并没有因为未使用类而不正确。函数已经能隔离局部参数,模块也能组织代码;类提供的是另一种组织方式:把一类实例的相关数据与操作放在同一个接口中。
当程序需要保存多份记录、执行多种相关操作或约定状态更新规则时,类通常有助于说明这些关系。类不会自动验证公式、阻止非法赋值,也不保证比函数版本更快。
类、实例与已有类型
类(class)定义一类对象的属性与行为;一个该类的具体对象称为实例(instance)。学生已经使用过对象:整数、字符串、列表、字典都有运行时类型,"abc".upper() 则是通过字符串对象访问方法。
print(type(42), type(3.5), type("abc"), type([1, 2]))
text = "hello world"
print(text.count("o"))
items = [3, 1, 2]
print(items.reverse())
print(items)
reverse() 修改列表并返回 None。调用的返回值与原对象的变化需要分别观察。
下面定义一个尚未增加属性或方法的类。pass 是空操作语句,用于需要语句块但暂不执行操作的位置。
class Point:
pass
p = Point()
print(type(p))
print(type(p) is Point)
执行 class 语句建立类,Point() 创建该类的一个实例。type(p) 为 Point,身份比较为 True。默认显示中的模块名与执行环境有关;对象地址也会随运行改变,不用固定地址判断程序正确性。
类本身也是对象,可以由名字 Point 引用。内置类型的空构造例子放在参考区。
属性与普通变量
本讲使用的普通自定义类允许通过赋值增加实例属性。p.x 表示对象 p 的属性,单独的变量 x 则是另一个名字:
class Point:
pass
x = 100
p = Point()
p.x = 3
p.y = 4
print(p.x, p.y, x)
结果为 3 4 100。设置 p.x 没有修改外部变量 x。
同类的另一个实例不会自动获得刚才添加的属性。以下程序有意触发 AttributeError:
class Point:
pass
p = Point()
p.x = 3
q = Point()
print(p.x)
print(q.x)
先打印 3,随后报错。实例属于同一个类,不表示程序已经给每个实例建立了相同的数据字段。
初始化与默认参数
在普通类的创建流程中,__init__ 初始化新实例。它的第一个参数接收该实例,惯用名称为 self。
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
for p in [Point(), Point(3), Point(3, 4)]:
print(p.x, p.y)
三行分别为 0 0、3 0、3 4。参数 x 是当前调用中的局部名字;self.x = x 将其值绑定到新实例的属性。默认参数规则与普通函数相同。
这里零坐标是合法默认值。其他类的默认值仍须符合自己的任务约定,例如三条零边不能表示一个合法三角形。
类体和方法体的执行时机
class 语句执行时运行类体;类体中的 def 建立函数,并不立即执行函数体。先预测下面的打印顺序:
class Trace:
print("class body")
def __init__(self) -> None:
print("init")
a = Trace()
b = Trace()
结果是 class body、init、init。这个类定义只执行了一次,随后创建两个实例,分别初始化。若程序再次执行同一段类定义,会重新建立类;不能把“本例类体执行一次”理解为所有程序中的类体永远只执行一次。
__init__ 必须返回 None,通常不写显式 return。调用 Point(...) 得到的结果是实例,不是 __init__ 的返回值。创建与初始化的区别及错误返回值例子在参考区说明。
别名和独立实例
赋值仍然绑定引用,不会因为使用类而自动复制对象:
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
p = Point(3, 4)
alias = p
other = Point(3, 4)
alias.x = 8
print(p.x, other.x)
print(alias is p, other is p)
结果为 8 3、True False。alias 与 p 引用同一个对象;other 是另一个实例,尽管初始坐标相同。is 检查对象身份,不检查两个点的几何坐标是否相等。自定义值相等协议留到 L10。
初始化时应明确必需属性、输入范围、默认值与失败行为。属性已经建立,不代表它的数值符合约定;类型提示也不会自动进行这种检查。
II. 实例方法与接收者
定义中的 self 与调用中的对象
普通实例方法的第一个参数接收调用者选择的实例。
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
def show(self) -> None:
print(self.x, self.y)
p = Point(3, 4)
p.show()
Point.show(p)
两次都打印 3 4。对这个普通 Python 方法,p.show() 绑定 p,也可以显式写 Point.show(p)。self 不是关键字,换成其他参数名在语法上可行,但应遵守通用命名惯例。
这一规则不能直接推广到所有方法类别。后面会看到类方法传入类,静态方法不自动传入接收者。
以下程序故意漏掉接收者参数,实例调用时产生 TypeError:
class Point:
def show():
print("hi")
p = Point()
p.show()
问题是绑定后传入了一个实例,而函数定义不接收参数,不是解释器要求参数必须拼写为 self。
局部参数和实例属性
同名的参数与属性可以有不同用途:
class Point:
def __init__(self, x: int) -> None:
self.x = x
def shift(self, x: int) -> None:
self.x += x
x = 100
p = Point(3)
p.shift(2)
print(p.x, x)
结果为 5 100。shift 的局部参数 x 是 2,self.x 原来是 3,外部变量 x 仍为 100。方法中的每个裸名字不会自动变成实例属性。
同一个函数,不同的接收者
类中定义的普通方法函数可以由多个实例使用;每次调用仍有自己的局部状态。
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
def show(self) -> None:
print(self.x, self.y)
a = Point(0, 0)
b = Point(3, 4)
a.show()
b.show()
先打印 0 0,再打印 3 4。不同结果来自两次不同的 self。这不意味着实例引用的全部数据都不共享;共享集合的情况在第四节讨论。
状态变化与返回值
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
def move(self, dx: float, dy: float) -> None:
self.x += dx
self.y += dy
p = Point(3, 4)
result = p.move(1, 0)
print(p.x, p.y, result)
结果为 4 4 None。move 修改接收者,结束时返回 None。返回 None 不表示调用没有副作用;返回数值的方法也不必然是只读的。
方法内部要选择当前实例的其他方法,仍须明确访问路径:
def message() -> str:
return "global function"
class Record:
def __init__(self, text: str) -> None:
self.text = text
def message(self) -> str:
return self.text
def report(self) -> str:
return self.message()
r = Record("instance method")
print(message())
print(r.report())
message() 与 self.message() 在本例访问不同的函数。完整的名称与作用域规则留到 L14;此处先正确区分局部名字和对象属性。
Point 的完整接口
distance_to 需要另一个 Point。Python 3.13 在执行类体时,类名尚未完成绑定,因此这里用字符串 "Point" 作为前向类型引用。可选的 future 功能在参考区介绍。
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
def show(self) -> None:
print(self.x, self.y)
def move(self, dx: float, dy: float) -> None:
self.x += dx
self.y += dy
def distance_to(self, other: "Point") -> float:
dx = self.x - other.x
dy = self.y - other.y
return (dx * dx + dy * dy) ** 0.5
a = Point(0, 0)
b = Point(3, 4)
print(a.distance_to(b))
a.move(1, 1)
a.show()
print(a.distance_to(b))
本例依次得到 5.0、1 1 和约 3.605551275463989。使用小规模有限数值说明几何和接口,不将该实现当作处理任意尺度的数值几何库。
| 操作 | 状态变化 | 返回或输出 |
|---|---|---|
Point(x, y) | 初始化新实例的坐标 | 类调用返回实例;init 自身返回 None |
show() | 不修改坐标 | 打印坐标,返回 None |
move(dx, dy) | 修改接收者坐标 | 返回 None |
distance_to(other) | 本实现不修改两个点 | 返回距离 |
接口文档与测试共同说明这些行为,方法名、返回注解或采用类都不能替代核验。
III. 封装与状态规则
公开接口与不变量
把相关的数据和操作组织起来称为封装(encapsulation)。公开接口说明调用者可使用哪些操作、需要提供什么输入、操作后会得到什么状态或结果。
不变量是约定操作应保持的状态条件。例如,账户余额非负。检查构造后的初始状态,再检查每个公开更新操作,才能说明在这些操作下规则得到保持。
普通 Python 属性可以直接赋值。类不会自动阻止 account.balance = -10 或修改公开的历史列表。这里的保证以遵守接口约定为前提,不是权限隔离或安全沙箱。
审查取款候选
下面是错误候选,不是推荐实现。先预测负取款后的结果:
class CandidateAccount:
def __init__(self, balance: int) -> None:
self.balance = balance
def withdraw(self, amount: int) -> bool:
if amount > self.balance:
return False
self.balance -= amount
return True
a = CandidateAccount(100)
print(a.withdraw(-20), a.balance)
实际为 True 120。候选只检查余额不足,没有拒绝非正金额。类定义和注解没有补上缺失的规则。
账户的受支持操作
教学账户使用整数分表示金额。初始余额是非负整数;存取金额是正整数。存款不返回余额,取款用布尔值报告成功;拒绝操作不修改余额或历史。账单只生成字符串。
class BankAccount:
"""A teaching account; amounts are integer cents."""
def __init__(self, owner: str, balance: int = 0) -> None:
if type(balance) is not int or balance < 0:
raise ValueError("invalid opening balance")
self.owner = owner
self.balance = balance
self.history: list[tuple[str, int]] = []
def deposit(self, amount: int) -> None:
if type(amount) is not int or amount <= 0:
return
self.balance += amount
self.history.append(("deposit", amount))
def withdraw(self, amount: int) -> bool:
if type(amount) is not int or amount <= 0 or amount > self.balance:
return False
self.balance -= amount
self.history.append(("withdraw", amount))
return True
def statement(self) -> str:
lines = [f"{self.owner}: {self.balance} cents"]
for action, amount in self.history:
lines.append(f" {action}: {amount} cents")
return "\n".join(lines)
a = BankAccount("Alex", 100)
b = BankAccount("Blake")
a.deposit(50)
print(a.withdraw(30), a.balance)
print(a.withdraw(1000), a.balance)
print(a.withdraw(-20), a.balance)
print(b.balance, b.history)
print(a.statement())
三次取款分别为 True 120、False 120、False 120。第二个账户仍为余额 0、空历史。每个初始化都新建自己的历史列表。
构造、存款和取款都验证各自接受的输入。这里显式检查整数类型,bool 不被当作金额;不能把这些检查归功于类型提示。真实金融软件还有审计、并发、货币与持久化等要求,本例不提供这些保证。
温度的初始值与更新
math.isfinite(value) 判断数值是否有限。这里用 float("inf") 构造正无穷,以检验这个条件:
import math
print(math.isfinite(25.0))
print(math.isfinite(float("inf")))
结果为 True、False。下例约定输入为实数,值须有限且不低于 −273.15°C。构造失败抛 ValueError,更新失败返回 False 并保留旧值。
内部字段 _celsius 使用单下划线,表示非公开实现约定;前缀本身不会阻止读取或写入。
import math
class Temperature:
"""Minimal lecture interface, not the full lab interface."""
def __init__(self, celsius: float) -> None:
if not math.isfinite(celsius) or celsius < -273.15:
raise ValueError("invalid starting temperature")
self._celsius = celsius
def get_celsius(self) -> float:
return self._celsius
def to_fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
def set_celsius(self, value: float) -> bool:
if not math.isfinite(value) or value < -273.15:
return False
self._celsius = value
return True
t = Temperature(25)
print(t.to_fahrenheit())
print(t.set_celsius(-300), t.get_celsius())
print(t.set_celsius(100), t.to_fahrenheit())
输出为 77.0、False 25、True 212.0。构造也须检查初值,不能只给 setter 加条件。实验在这个最小接口基础上另要求摄氏度、华氏度和 Kelvin 三种查询,名称以实验页为准。
单下划线与名称改写
_name 表示内部实现,调用者不应依赖其结构或绕过公开接口直接更新。Python 不会凭这个前缀强制禁止访问。
类体中的双前导下划线名称,如 __hidden,会发生名称改写(name mangling)。下面的普通属性确实可以通过改写后的名字读取:
class Secret:
def __init__(self) -> None:
self.__hidden = 42
s = Secret()
print(s._Secret__hidden)
结果为 42。下面独立程序故意使用未改写名字,得到 AttributeError:
class Secret:
def __init__(self) -> None:
self.__hidden = 42
s = Secret()
print(s.__hidden)
名称改写主要帮助避免继承中的无意同名冲突,不是安全访问边界;继承例子留到 L10。
| 形式 | 本讲含义 |
|---|---|
_x | 非公开接口的命名约定 |
__x | 类体中的名称改写 |
__init__ | Python 文档定义的特殊方法名,不按 __x 的规则改写 |
不是任意自造的双端下划线名字都会获得解释器的特殊行为。
IV. 共享数据与方法类别
实例属性与类属性
实例属性通常保存该实例的数据;类属性由类体或对类的赋值建立。对本讲没有描述符或继承参与的普通属性,实例没有对应绑定时,可以访问类上的绑定。
class Point:
dimensions = 2
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
a = Point(0, 0)
b = Point(3, 4)
print(a.x, b.x)
print(a.dimensions, b.dimensions, Point.dimensions)
坐标不同,三种维度访问均为 2。实例写入同名普通属性则可以遮蔽类上的绑定:
class Record:
label = "class"
a = Record()
b = Record()
a.label = "only A"
print(a.label, b.label, Record.label)
结果为 only A class class。a.label = ... 没有修改类上的 Record.label。这里不是包含数据描述符、property 和继承的完整属性查找规则,那些机制在 L10 继续讨论。
修改列表与重新绑定属性
下面的类变量只引用一个列表:
class Basket:
items: list[str] = []
def add(self, item: str) -> None:
self.items.append(item)
a = Basket()
b = Basket()
a.add("apple")
print(b.items)
print(a.items is b.items)
结果为 ['apple']、True。self.items.append(...) 修改被找到的列表,并没有自动建立一个独立的实例列表。如果接口要求不同篮子独立,这就是错误;如果设计本来需要共同登记表,共享类属性也可能合理。
每次初始化创建新列表,可满足此处独立篮子的约定:
class Basket:
def __init__(self) -> None:
self.items: list[str] = []
def add(self, item: str) -> None:
self.items.append(item)
a = Basket()
b = Basket()
a.add("apple")
print(a.items, b.items)
print(a.items is b.items)
得到 ['apple'] []、False。独立性来自分别创建新列表,不来自参数名字 self。
输入共享与可变默认参数
即使字段写在 init 内,直接绑定同一个输入仍会共享:
class Bucket:
def __init__(self, items: list[str]) -> None:
self.items = items
shared = []
a = Bucket(shared)
b = Bucket(shared)
a.items.append("apple")
print(b.items)
print(a.items is b.items)
结果仍为 ['apple']、True。需要隔离外层列表时,可以按接口选择 items.copy();如果元素本身可变,浅拷贝不保证内层独立。
可变默认参数是第三种来源,沿用 L04 的规则:
class Bucket:
def __init__(self, items: list[str] = []) -> None:
self.items = items
a = Bucket()
b = Bucket()
a.items.append("apple")
print(b.items)
print(a.items is b.items)
默认表达式在定义函数时求值,两个不传参数的调用使用同一个默认列表。类级字段、默认参数和重复传入的输入,必须分别分析。
类级计数与类方法
共享创建计数应更新类上的绑定,而不是意外建立一个实例同名属性:
class Counter:
total = 0
def add(self) -> None:
self.total += 1
a = Counter()
b = Counter()
a.add()
print(a.total, b.total, Counter.total)
结果为 1 0 0。self.total += 1 在本例读取类上的 0,再把 1 绑定到 a.total,没有更新共享的 Counter.total。
类方法由内置标记 @classmethod 定义。它自动接收类,惯用参数名为 cls;通过类调用时无需先创建实例。这一节先学习该标记的具体作用,通用装饰器机制仍在 L14 讨论。
class Student:
count = 0
def __init__(self, name: str) -> None:
self.name = name
Student.count += 1
@classmethod
def created_count(cls) -> int:
return cls.count
print(Student.created_count())
a = Student("Alex")
b = Student("Blake")
print(Student.created_count(), a.created_count())
结果为 0、2 2。从实例访问这个类方法,自动传入的仍是类,而不是 a。计数表示执行这个初始化过程的次数,不是当前存活对象数;删除引用不会自动减少这个普通类字段。若初始化可能失败,应明确计数更新放在成功校验之后。
| 类别 | 自动参数 | 用途 |
|---|---|---|
| 普通实例方法 | 实例,惯用名 self | 操作指定实例 |
类方法 @classmethod | 类,惯用名 cls | 类级操作或查询 |
静态方法 @staticmethod | 无自动接收者 | 与类有关但不需实例/类状态的操作 |
静态方法例子在参考区先解释标记,再展示完整程序。某个函数被放进类中,并不意味着它必须访问实例状态。
参考:完整例子与核验
内置构造器与类型
__name__ 给出这里的类名,repr() 显示对象的表示,便于看出空字符串与空容器:
classes = [int, float, complex, str,
list, tuple, dict, set]
for cls in classes:
value = cls()
print(cls.__name__, repr(value))
这些构造产生 0、0.0、0j、空字符串和相应空容器。名字重新绑定到另一类型的对象,不表示原对象的类型被修改。
Triangle:验证、面积与方法内调用
以下版本默认三边为 1。初始三边须有限、正且满足严格三角不等式。更新失败返回 False,不改变旧边;area 仍检查当前状态,因为外部直接改公开字段可能绕过 setter。示例使用尺度适中的有限数值,未提供通用几何数值稳定性保证。
import math
class Triangle:
def __init__(self, a: float = 1, b: float = 1, c: float = 1) -> None:
self.a, self.b, self.c = a, b, c
if not self.is_valid():
raise ValueError("invalid triangle")
def is_valid(self) -> bool:
for value in [self.a, self.b, self.c]:
if not math.isfinite(value) or value <= 0:
return False
return (self.a + self.b > self.c and self.b + self.c > self.a
and self.c + self.a > self.b)
def set_edges(self, a: float, b: float, c: float) -> bool:
for value in [a, b, c]:
if not math.isfinite(value) or value <= 0:
return False
if not (a + b > c and b + c > a and c + a > b):
return False
self.a, self.b, self.c = a, b, c
return True
def area(self) -> float:
if not self.is_valid():
raise ValueError("invalid current edges")
s = (self.a + self.b + self.c) / 2
product = s * (s - self.a)
product *= s - self.b
product *= s - self.c
return product ** 0.5
def print(self) -> None:
print(f"Triangle: {self.a}, {self.b}, {self.c}")
def print_area(self) -> None:
print(f"Area: {self.area():.3f}")
a = Triangle(3, 4, 5)
b = Triangle(3, 3, 3)
a.print()
a.print_area()
print(b.area())
print(a.set_edges(1, 1, 3), a.area())
三四五三角形面积为 6;等边长 3 的面积约为 3.897114317029974。set_edges(1,1,3) 被拒绝,原面积仍为 6。旧材料中零边默认值虽能建立字段,却不表示合法三角形。
print_area 使用 self.area() 调用当前实例的方法。外部也可以定义名为 area 的普通函数,两个接口仍须通过明确的名字或对象访问路径区分。
Dog:字段、外部函数与方法
保留往年的记录例子,用来比较接口组织,不需要依赖动物照片或名称类比。
class Dog:
def __init__(self, name: str, color: str, age: int, weight: float) -> None:
self.name = name
self.color = color
self.age = age
self.weight = weight
def set_name(self, name: str) -> None:
self.name = name
def set_color(self, color: str) -> None:
self.color = color
def set_age(self, age: int) -> None:
self.age = age
def set_weight(self, weight: float) -> None:
self.weight = weight
def print_dog(self) -> None:
print(
self.name, self.color,
self.age, self.weight,
)
def train_dog(dog: Dog) -> None:
dog.name += " 1st"
dog = Dog("Fox", "Red", 3, 4)
dog.print_dog()
dog.age += 1
dog.weight /= 2
train_dog(dog)
dog.print_dog()
dog.set_name("Tiger")
dog.print_dog()
外部函数也能接受对象,方法是另一种相关操作的组织方式。setter 这个名字本身不保证验证;本例只展示记录更新,没有宣称检查年龄或体重的业务规则。
HappyNumber:数位平方和与循环检测
正整数反复替换为各十进制数位的平方和,最终达到 1 则为快乐数;出现重复值且未到 1,之后会重复同一过程。集合保存已经处理的数,局部变量保存本次查询的工作状态。
class HappyNumber:
def __init__(self, number: int) -> None:
if number <= 0:
raise ValueError("positive integer required")
self.number = number
def sum_of_digit_squares(self) -> int:
return self._digit_square_sum(self.number)
def _digit_square_sum(self, value: int) -> int:
total = 0
while value > 0:
digit = value % 10
total += digit * digit
value //= 10
return total
def is_happy(self) -> bool:
current = self.number
seen = set()
while current not in seen:
if current == 1:
return True
seen.add(current)
current = self._digit_square_sum(current)
return False
def print_HN(self) -> None:
print(self.number)
for number in [19, 2, 1234567890987654321]:
h = HappyNumber(number)
print(h.number, h.sum_of_digit_squares(), h.is_happy())
print(h.number == number)
三次首步分别为 82、4、570;判定为 True、False、False,原 number 都保持。19 的轨迹为 19→82→68→100→1;2 经过 2→4→16→37→58→89→145→42→20→4 出现重复。
旧候选的 is_happy 更新 self.number,对 19 返回 True 时字段已经变成 100。方法名是查询形式,并不使它自动只读。把这一行为与本版比较时,应分别核对返回值和原始字段,不能只比较 True/False。
这个判定也可写成普通函数。采用类的理由取决于是否需要保留原数、相关查询与长期接口,不是因为循环检测必须用类。
Vector:长度、方向、点积与伸缩
使用 n 维标量列表。构造复制外层列表;length 查询范数,dot 要求维度一致,scale 原地修改。angle() 定义为与各坐标轴的夹角列表,单位为弧度;零向量方向未定义,抛 ValueError。
math.acos(x) 是主值反余弦,输入在 −1 到 1,结果在 0 到 π。各轴方向余弦为坐标分量除以长度。这里将舍入导致的微小越界钳位到定义域,示例仍限于尺度适中的有限标量,不承诺极端数值下的通用稳定性。
import math
class Vector:
def __init__(self, values: list[float]) -> None:
self.values = values.copy()
def length(self) -> float:
total = 0.0
for value in self.values:
total += value * value
return math.sqrt(total)
def print_length(self) -> None:
print(f"Length: {self.length():.3f}")
def dot(self, other: "Vector") -> float:
if len(self.values) != len(other.values):
raise ValueError("different dimensions")
total = 0.0
for i in range(len(self.values)):
total += self.values[i] * other.values[i]
return total
def scale(self, ratio: float) -> None:
for i in range(len(self.values)):
self.values[i] *= ratio
def angle(self) -> list[float]:
length = self.length()
if length == 0:
raise ValueError("zero vector")
angles = []
for value in self.values:
direction = value / length
cosine = max(-1.0, min(1.0, direction))
angles.append(math.acos(cosine))
return angles
def print_angle(self) -> None:
for angle in self.angle():
print(f"Angle: {angle:.4f} rad")
values = [3.0, 4.0]
v = Vector(values)
u = Vector([1.0, 2.0])
v.print_length()
print(v.dot(u))
v.print_angle()
print(v.scale(2))
print(v.values, values, v.length())
长度为 5.0,点积为 11.0;两轴夹角约为 0.9273、0.6435 弧度。伸缩返回 None,v 的分量变成 [6.0,8.0],输入 values 保持 [3.0,4.0],新长度 10.0。
浅拷贝足以隔离本例的外层标量列表;不能推广为任意嵌套可变元素都独立。空向量长度为 0;不等维 dot 与零向量 angle 的失败行为需分别测试。
同名操作与函数对象
下面两个 area 名称有明确访问路径。普通函数接收三条边,实例方法从自身字段取得参数,再调用外部函数;使用类没有把裸名字 area 自动改成 self.area。
def area(a: float, b: float, c: float) -> float:
s = (a + b + c) / 2
return (s * (s - a) * (s - b) * (s - c)) ** 0.5
class Shape:
def __init__(self, a: float, b: float, c: float) -> None:
self.a, self.b, self.c = a, b, c
def area(self) -> float:
return area(self.a, self.b, self.c)
x = Shape(3, 4, 5)
print(area(3, 4, 5), x.area())
f = print
f("function object")
两个面积都是 6.0。f 绑定函数对象而非 print 的返回值,最后的调用正常打印字符串。函数与类都是组织工具,不能把局部参数隔离归为类独有的能力。
绑定方法的元数据与函数对象
对本例中 Python 定义的普通方法,__self__ 给出绑定接收者,__func__ 给出类中底层函数。先理解这两个属性,再查看结果:
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x
self.y = y
def show(self) -> None:
print(self.x, self.y)
a = Point(0, 0)
b = Point(3, 4)
print(a.show.__self__ is a)
print(a.show.__func__ is b.show.__func__)
f = a.show
a.x = 8
f()
身份比较均为 True,最后打印 8 0。保存的是绑定当前对象的方法,之后对象状态的变化仍能被调用观察。这些元数据不直接推广到所有内置方法。
函数本身也是对象。f = print 是把同一个函数对象绑定给另一个名字,f("hello") 可以调用它;这里没有执行 print 并保存其返回值。
无隐式接收者的操作
先解释标记:@staticmethod 使函数通过类或实例访问时不自动绑定接收者。
class Test:
@staticmethod
def add(u: int, v: int) -> int:
return u + v
print(Test.add(1, 1))
print(Test().add(3, 4))
输出为 2、7。若去掉标记而仍定义 add(u,v),Test.add(1,1) 可以显式调用,但 Test().add(3,4) 会额外传入实例而参数过多。不能笼统声称“函数未写 self 就完全无法使用”。
不依赖实例或类状态的工具函数,也可以放在类外。方法类别是一项接口选择,不是把每个函数移进类的理由。
前向注解与初始化失败
字符串前向引用是本讲主线用法。from __future__ import annotations 从 Python 3.7 起是可选功能,Python 3.13 中仍需显式启用;它可以推迟注解求值,而不是校验实参。
普通创建流程通常先由 __new__ 创建实例,再由 __init__ 初始化。这里不实现这些创建钩子;有的资料将 init 简称构造函数,阅读时应注意两阶段区别。
以下程序有意触发 TypeError:
class Wrong:
def __init__(self):
return 1
Wrong()
另两种常见问题是把 __init__ 拼成普通名字,以及忘记接收者参数。前者不会自动执行该初始化代码,后者造成参数数量不匹配。省略自定义 init 仍可有默认创建行为,但不会替程序添加预期的 x/y 等字段。
组织方式与选择依据
面向对象思想在 20 世纪 60 年代的 Simula 等语言中形成重要机制,Smalltalk 在 70 年代推动其发展。不能据此声称 OO 取代了全部编程方式。Python 同时支持对象与过程式组织;C++、Java 是其他常见相关语言。
决定是否使用类时,考虑状态是否跨调用保留、操作是否紧密相关、生命周期与有效性规则是否值得一个明确接口。函数、字典、元组与结构化记录也可能合适。只读数据或只有一个实例不自动排除类,只有一个方法也不自动证明类多余。
NamedTuple 是结构化记录的一种候选,此处只介绍名称与适用性,不提前使用该 API。
小结与练习
- 实例属性、局部参数与类属性有不同绑定位置。
- 普通实例方法绑定对象;类方法绑定类;静态方法没有自动接收者。
- 初始化、更新、返回值、输出及副作用分别核验。
- 类级列表、可变默认参数与重复传入的列表是三种不同共享来源。
- 下划线约定与名称改写都不是安全权限控制。
- 函数、记录与类是组织方式的候选,选择需要接口和状态依据。
练习:
- 给 Point 增加一个返回新点的平移操作,说明它与原地 move 的接口区别,检查输入是否保持。
- 用 Triangle 的两个实例验证合法/非法 set_edges,并检查失败后原三边不变。
- 验证 HappyNumber 查询前后 number,记录实际有序轨迹,不依赖 set 的显示顺序。
- 检查 Vector 的零向量、维度不等、输入列表与原地伸缩。说明 direction 的弧度约定。
- 保存一次绑定方法后修改实例状态,再调用它,解释接收者和字段变化。
- 比较转换函数、牌组、回文判定和持有对话历史的程序的组织需求;不实现外部模型调用。
Lab 9 保留 Rectangle、Stack、WordCounter、Temperature、Gradebook 和设计判断六项任务,具体方法名及失败行为以实验页为准。