深入理解python的面向对象编程
- 一、面向对象编程
-
- [1. 类与对象](#1. 类与对象)
-
- [1.1 什么是类](#1.1 什么是类)
- [1.2 创建对象](#1.2 创建对象)
- [1.3 对象保存自己的数据](#1.3 对象保存自己的数据)
- [2. 初始化方法与 self](#2. 初始化方法与 self)
-
- [2.1 使用init初始化对象](#2.1 使用init初始化对象)
- [2.2 self 表示当前实例](#2.2 self 表示当前实例)
- [2.3 不要遗漏 self](#2.3 不要遗漏 self)
- [2.4 init不返回业务结果](#2.4 init不返回业务结果)
- [3. 实例属性与类属性](#3. 实例属性与类属性)
-
- [3.1 实例属性:每个对象各自拥有](#3.1 实例属性:每个对象各自拥有)
- [3.2 类属性:定义在类本身上的数据](#3.2 类属性:定义在类本身上的数据)
- [3.3 实例赋值可能遮蔽类属性](#3.3 实例赋值可能遮蔽类属性)
- [3.4 不要把每个实例的数据放进可变类属性](#3.4 不要把每个实例的数据放进可变类属性)
- [4. 实例方法、类方法与静态方法](#4. 实例方法、类方法与静态方法)
-
- [4.1 实例方法](#4.1 实例方法)
- [4.2 类方法](#4.2 类方法)
- [4.3 静态方法](#4.3 静态方法)
- [4.4 三种方法对比](#4.4 三种方法对比)
- [5. 属性管理与 property](#5. 属性管理与 property)
-
- [5.1 直接公开属性的问题](#5.1 直接公开属性的问题)
- [5.2 使用 property 校验赋值](#5.2 使用 property 校验赋值)
- [5.3 避免在 property 中递归访问自己](#5.3 避免在 property 中递归访问自己)
- [5.4 只读属性](#5.4 只读属性)
- [5.5 单下划线与双下划线](#5.5 单下划线与双下划线)
- [6. 继承与方法重写](#6. 继承与方法重写)
-
- [6.1 继承已有行为](#6.1 继承已有行为)
- [6.2 方法重写](#6.2 方法重写)
- [6.3 super() 的含义](#6.3 super() 的含义)
- [6.4 isinstance 与 type](#6.4 isinstance 与 type)
- [7. 多态与鸭子类型](#7. 多态与鸭子类型)
-
- [7.1 相同操作,不同实现](#7.1 相同操作,不同实现)
- [7.2 使用抽象基类明确约定](#7.2 使用抽象基类明确约定)
- [8. 组合:让对象协作](#8. 组合:让对象协作)
-
- [8.1 继承与组合的区别](#8.1 继承与组合的区别)
- [8.2 组合示例](#8.2 组合示例)
- [9. 常用特殊方法](#9. 常用特殊方法)
-
- [9.1 str:面向使用者的字符串](#9.1 str:面向使用者的字符串)
- [9.2 repr:便于调试的表示](#9.2 repr:便于调试的表示)
- [9.3 len:支持 len()](#9.3 len:支持 len())
- [9.4 eq:定义相等规则](#9.4 eq:定义相等规则)
- [10. dataclass:简化数据类](#10. dataclass:简化数据类)
-
- [10.1 自动生成常见方法](#10.1 自动生成常见方法)
- [10.2 类型注解不等于运行时校验](#10.2 类型注解不等于运行时校验)
- [10.3 可变默认值使用 default_factory](#10.3 可变默认值使用 default_factory)
- [10.4 frozen=True](#10.4 frozen=True)
如需转载,请附上链接:https://zhenghuisheng.blog.csdn.net/article/details/166797802
一、面向对象编程
前几篇,我们已经学习了数据容器、函数、异常处理和模块组织,并完成了一个能够读取、筛选和统计商品的程序。在这个程序中,商品数据保存在字典里,查询和修改操作放在函数中:
python
product = {
"name": "矿泉水",
"price": 2,
"stock": 100
}
这种方式简单直接,适合很多小型程序。但当一个商品需要维护更多规则时,例如库存不能为负数、出库不能超过现有库存、名称不能为空,就需要考虑如何把数据和相关操作组织起来。
面向对象提供了另一种组织方式:用对象保存状态,用方法描述操作,用类定义这类对象共同遵循的结构和行为。
1. 类与对象
1.1 什么是类
类用于定义一类对象共同具有的数据和行为。例如,商品通常具有:名称、单价、库存 ;商品还可以执行一些操作:增加库存、减少库存、计算指定数量的金额 可以使用 class 定义一个商品类:
python
class Product:
pass
类名通常使用大驼峰命名,例如:
Product
ShoppingCart
InventoryService
1.2 创建对象
定义类后,通过 类名() 创建实例:
python
class Product:
pass
if __name__ == '__main__':
first = Product()
second = Product()
print(type(first))
print(first is second)
运行结果:
<class '__main__.Product'>
False
first 和 second 都是 Product 的实例,但它们是两个不同的对象。几个概念可以这样区分:
| 概念 | 含义 |
|---|---|
| 类 | 定义对象共同的结构和行为 |
| 对象 | 程序中具体存在的实体 |
| 实例 | 强调某个对象是由某个类创建的 |
1.3 对象保存自己的数据
普通自定义类的实例,通常允许直接添加属性:
python
class Product:
pass
if __name__ == '__main__':
first = Product()
first.name = "矿泉水"
first.stock = 100
second = Product()
second.name = "可乐"
second.stock = 20
print(first.name, first.stock)
print(second.name, second.stock)
运行结果:
矿泉水 100
可乐 20
但这种写法要求调用方手动补齐属性,容易遗漏,也容易拼错名称。
2. 初始化方法与 self
2.1 使用init初始化对象
python
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
if __name__ == '__main__':
product = Product("矿泉水", 2, 100)
print(product.name)
print(product.price)
print(product.stock)
运行结果:
矿泉水
2
100
Product("矿泉水", 2, 100)
时,Python 通常先创建实例,再调用 __init__() 初始化它。严格来说,__init__ 负责初始化,不负责分配和创建实例本身。实例创建涉及 __new__,一般业务类暂时不需要自定义它。
2.2 self 表示当前实例
self.name = name
可以拆成两部分:
- 右侧
name:本次调用传入的参数。 - 左侧
self.name:当前对象上的属性。
self 是实例方法的第一个参数,调用时由 Python 自动传入当前实例。
python
class Product:
def __init__(self, name):
self.name = name
def show_name(self):
print(self.name)
if __name__ == '__main__':
product = Product("矿泉水")
product.show_name()
运行结果:
矿泉水
对于这里的普通实例方法:
product.show_name()
可以近似理解为:
Product.show_name(product)
因此,调用时不需要再手动传入 self。self 不是关键字,但它是约定俗成的名称,不应随意改成其他名字。
2.3 不要遗漏 self
错误写法:
python
class Product:
def show_name():
print("商品")
通过实例调用时,Python 会自动传入实例,但函数没有参数接收,因而出现 TypeError。正确写法:
python
class Product:
def show_name(self):
print("商品")
2.4 init不返回业务结果
__init__() 应返回 None,通常不写 return。不要写成:
python
class Product:
def __init__(self, name):
self.name = name
return name
这会导致 TypeError。如果需要返回其他计算结果,应定义普通方法;如果需要提供另一种对象创建方式,可以使用后面介绍的类方法。
3. 实例属性与类属性
3.1 实例属性:每个对象各自拥有
通过 self.xxx 赋值,通常会建立实例属性:
python
class Product:
def __init__(self, name, stock):
self.name = name
self.stock = stock
if __name__ == '__main__':
first = Product("矿泉水", 100)
second = Product("可乐", 20)
first.stock = 80
print(first.stock)
print(second.stock)
运行结果:
80
20
修改第一个商品的库存,不会改变第二个商品的库存。
3.2 类属性:定义在类本身上的数据
python
class Product:
unit = "件"
def __init__(self, name):
self.name = name
if __name__ == '__main__':
first = Product("矿泉水")
second = Product("可乐")
print(Product.unit)
print(first.unit)
print(second.unit)
运行结果:
件
件
件
unit 定义在类体中,是类属性。对于这里的普通属性,实例没有同名属性时,可以通过实例访问类属性。
3.3 实例赋值可能遮蔽类属性
python
class Product:
unit = "件"
if __name__ == '__main__':
first = Product()
second = Product()
first.unit = "箱"
print(first.unit)
print(second.unit)
print(Product.unit)
Product.unit = "瓶"
print(first.unit)
print(second.unit)
运行结果:
箱
件
件
箱
瓶
first.unit = "箱" 在 first 上建立了同名实例属性,遮蔽了通过它访问到的类属性。之后修改 Product.unit:
second没有自己的unit,所以读到新的类属性。first已有自己的unit,仍然返回"箱"。
后面介绍的 property 具有专门的属性访问规则,不应简单套用普通属性的遮蔽方式。
3.4 不要把每个实例的数据放进可变类属性
下面的代码容易产生共享数据问题:
python
class ShoppingCart:
items = []
def add(self, name):
self.items.append(name)
if __name__ == '__main__':
first = ShoppingCart()
second = ShoppingCart()
first.add("矿泉水")
print(first.items)
print(second.items)
运行结果:
['矿泉水']
['矿泉水']
两个对象访问的是同一个类级列表。如果购物车内容应当各自独立,就应该在 __init__ 中创建:
python
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
if __name__ == '__main__':
first = ShoppingCart()
second = ShoppingCart()
first.add("矿泉水")
print(first.items)
print(second.items)
运行结果:
['矿泉水']
[]
4. 实例方法、类方法与静态方法
4.1 实例方法
实例方法接收 self,适合读取或修改某个对象的状态。
python
class Product:
def __init__(self, name, stock=0):
self.name = name
self.stock = stock
def restock(self, quantity):
if type(quantity) is not int or quantity <= 0:
raise ValueError("入库数量必须是正整数")
self.stock += quantity
if __name__ == '__main__':
product = Product("矿泉水", 10)
product.restock(5)
print(product.stock)
运行结果:
15
方法中的 return、参数、异常处理等规则,与普通函数一致。
4.2 类方法
使用 @classmethod 定义类方法,第一个参数通常命名为 cls,表示接收的类。
python
class Product:
def __init__(self, name, stock=0):
self.name = name
self.stock = stock
@classmethod
def from_dict(cls, data):
return cls(
name=data["name"],
stock=data.get("stock", 0)
)
if __name__ == '__main__':
data = {
"name": "矿泉水",
"stock": 100
}
product = Product.from_dict(data)
print(product.name)
print(product.stock)
运行结果:
矿泉水
100
这里的:
return cls(...)
表示通过当前接收的类创建实例。类方法常用于提供额外的创建入口,例如:
Product.from_dict(...)
Product.from_text(...)
@classmethod 是一种装饰器写法。
4.3 静态方法
使用 @staticmethod 定义静态方法,它不会自动接收 self 或 cls。
python
class Product:
@staticmethod
def is_valid_name(name):
return isinstance(name, str) and bool(name.strip())
if __name__ == '__main__':
print(Product.is_valid_name("矿泉水"))
print(Product.is_valid_name(" "))
运行结果:
True
False
静态方法适合放置与类概念紧密相关、但不依赖实例或类状态的辅助逻辑。
4.4 三种方法对比
| 方法类型 | 自动接收 | 常见用途 |
|---|---|---|
| 实例方法 | self,当前实例 |
读取、修改对象状态 |
| 类方法 | cls,当前类 |
提供替代创建方式、处理类级逻辑 |
| 静态方法 | 无 | 与类相关的独立辅助操作 |
5. 属性管理与 property
5.1 直接公开属性的问题
下面的商品对象没有保护库存规则:
python
class Product:
def __init__(self, stock):
self.stock = stock
外部可以直接写:
python
product = Product(10)
product.stock = -100
如果库存不允许为负数,这个对象就进入了不合法状态。可以通过 property 在保留属性访问形式的同时,增加读取和赋值逻辑。
5.2 使用 property 校验赋值
python
class Product:
def __init__(self, name, stock=0):
self.name = name
self.stock = stock
@property
def stock(self):
return self._stock
@stock.setter
def stock(self, value):
if type(value) is not int or value < 0:
raise ValueError("库存必须是非负整数")
self._stock = value
if __name__ == '__main__':
product = Product("矿泉水", 10)
print(product.stock)
product.stock = 20
print(product.stock)
try:
product.stock = -1
except ValueError as error:
print(error)
print(product.stock)
运行结果:
10
20
库存必须是非负整数
20
这里:
- 读取
product.stock时,调用 getter。 - 执行
product.stock = value时,调用 setter。 - 实际数据保存在
_stock中。 - 校验失败发生在赋值之前,因此原库存保持不变。
初始化中的:
self.stock = stock
也会经过 setter,避免初始化时绕过校验。
5.3 避免在 property 中递归访问自己
错误写法:
python
@property
def stock(self):
return self.stock
读取 self.stock 又会调用 getter,形成不断递归。setter 中也不能这样写:
python
@stock.setter
def stock(self, value):
self.stock = value
因此通常使用另外一个内部属性保存数据:
python
self._stock
5.4 只读属性
只定义 getter,不定义 setter,可以提供只读的属性接口。
python
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
@property
def inventory_value(self):
return self.price * self.stock
if __name__ == '__main__':
product = Product("矿泉水", 2, 100)
print(product.inventory_value)
product.stock = 80
print(product.inventory_value)
运行结果:
200
160
inventory_value 每次读取时根据当前状态计算,不需要额外维护一份容易过期的数据。不能直接赋值:
product.inventory_value = 999
因为没有 setter,会抛出 AttributeError。不过,"只读属性"不等于整个对象不可变。这里仍然可以修改 price 和 stock。
5.5 单下划线与双下划线
Python 中常见的名称约定:
| 写法 | 含义 |
|---|---|
name |
普通公开名称 |
_name |
约定为内部实现,外部通常不直接使用 |
__name |
在类中触发名称改写,主要用于减少继承时的意外冲突 |
__name__ |
特殊方法或特殊属性的命名形式 |
单下划线不是访问权限控制:
product._stock
在普通情况下仍然能够被外部访问。双下划线也不是严格的安全隔离:
python
class Example:
def __init__(self):
self.__value = 10
这个名称会被改写为类似:
_Example__value
6. 继承与方法重写
6.1 继承已有行为
继承允许一个类复用另一个类的行为。
python
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def describe(self):
return f"{self.name},单价 {self.price} 元"
class DigitalProduct(Product):
def __init__(self, name, price, download_url):
super().__init__(name, price)
self.download_url = download_url
if __name__ == '__main__':
product = DigitalProduct(
"Python 学习资料",
20,
"https://example.com/python"
)
print(product.describe())
print(product.download_url)
运行结果:
Python 学习资料,单价 20 元
https://example.com/python
DigitalProduct(Product) 表示 DigitalProduct 继承 Product。
super().__init__(name, price)
调用继承链上的初始化逻辑,避免重复设置 name 和 price。如果子类自定义了 __init__,需要的父类初始化不会自动再执行,应明确调用。
6.2 方法重写
子类可以重新定义同名方法:
python
class Product:
def __init__(self, name):
self.name = name
def delivery_info(self):
return "需要安排实物配送"
class DigitalProduct(Product):
def delivery_info(self):
return "通过下载链接交付"
if __name__ == '__main__':
first = Product("矿泉水")
second = DigitalProduct("Python 学习资料")
print(first.delivery_info())
print(second.delivery_info())
运行结果:
需要安排实物配送
通过下载链接交付
调用形式相同,但不同类型的对象提供了不同实现。
6.3 super() 的含义
在简单单继承中,可以先把 super() 理解为访问继承链上的下一层实现。
但它并不只是"写死调用某个父类"。Python 支持多继承,super() 会按照方法解析顺序,也就是 MRO,继续查找。。
6.4 isinstance 与 type
python
class Product:
pass
class DigitalProduct(Product):
pass
if __name__ == '__main__':
product = DigitalProduct()
print(isinstance(product, DigitalProduct))
print(isinstance(product, Product))
print(type(product) is Product)
运行结果:
True
True
False
isinstance()考虑继承关系。type(obj) is SomeClass检查对象是否恰好属于这个类。
需要允许子类参与时,isinstance() 通常更合适。
7. 多态与鸭子类型
7.1 相同操作,不同实现
如果多个对象都提供某个方法,调用方就可以按照同一种方式使用它们。
python
class ConsoleNotifier:
def send(self, message):
print(f"控制台通知:{message}")
class MemoryNotifier:
def __init__(self):
self.messages = []
def send(self, message):
self.messages.append(message)
def notify_low_stock(notifier, product_name):
notifier.send(f"{product_name} 库存不足")
if __name__ == '__main__':
console = ConsoleNotifier()
memory = MemoryNotifier()
notify_low_stock(console, "矿泉水")
notify_low_stock(memory, "矿泉水")
print(memory.messages)
运行结果:
控制台通知:矿泉水 库存不足
['矿泉水 库存不足']
notify_low_stock() 不需要知道传入对象的具体类,只需要它提供能够按约定调用的 send()。这就是 Python 中常说的鸭子类型:关注对象能提供什么行为,而不只是它继承自哪个类。
7.2 使用抽象基类明确约定
如果需要明确要求子类实现某个方法,可以使用标准库 abc:
python
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message):
"""发送消息。"""
class ConsoleNotifier(Notifier):
def send(self, message):
print(f"通知:{message}")
if __name__ == '__main__':
notifier = ConsoleNotifier()
notifier.send("库存不足")
运行结果:
通知:库存不足
直接实例化仍有抽象方法未实现的类,会抛出 TypeError。抽象基类用于明确接口约定,但不会自动验证实现是否满足所有业务语义。小型程序中,如果鸭子类型已经足够清楚,不必为每个类都增加抽象层。
8. 组合:让对象协作
8.1 继承与组合的区别
继承通常表达"是一种"关系:
数字商品是一种商品
组合通常表达"拥有或使用"关系:
库存服务使用通知器
当一个对象只是需要借助另一个对象完成工作,组合通常更直接。
8.2 组合示例
python
class ConsoleNotifier:
def send(self, message):
print(f"通知:{message}")
class InventoryService:
def __init__(self, notifier):
self.notifier = notifier
def check_stock(self, product_name, stock):
if stock < 5:
self.notifier.send(f"{product_name} 库存不足")
if __name__ == '__main__':
notifier = ConsoleNotifier()
service = InventoryService(notifier)
service.check_stock("矿泉水", 3)
运行结果:
通知:矿泉水 库存不足
InventoryService 没有继承通知器,只是在内部保存并使用它。
以后换成别的通知实现,只要仍然提供兼容的 send(),库存服务就不必跟着修改
9. 常用特殊方法
9.1 str:面向使用者的字符串
直接打印普通对象时,默认结果通常包含类名和地址信息,不方便阅读。
可以定义 __str__():
python
class Product:
def __init__(self, name, stock):
self.name = name
self.stock = stock
def __str__(self):
return f"{self.name},库存 {self.stock}"
if __name__ == '__main__':
product = Product("矿泉水", 100)
print(product)
print(str(product))
运行结果:
矿泉水,库存 100
矿泉水,库存 100
__str__() 必须返回字符串,而不是直接打印内容。
9.2 repr:便于调试的表示
python
class Product:
def __init__(self, name, stock):
self.name = name
self.stock = stock
def __str__(self):
return f"{self.name},库存 {self.stock}"
def __repr__(self):
return f"Product(name={self.name!r}, stock={self.stock!r})"
if __name__ == '__main__':
product = Product("矿泉水", 100)
print(product)
print(repr(product))
print([product])
运行结果:
矿泉水,库存 100
Product(name='矿泉水', stock=100)
[Product(name='矿泉水', stock=100)]
可以这样区分:
| 方法 | 常见用途 |
|---|---|
__str__ |
面向使用者的简洁展示 |
__repr__ |
面向开发者的调试表示 |
容器展示内部对象时通常使用其 repr 表示。
如果没有定义 __str__,默认字符串展示可以退回使用 __repr__。
9.3 len:支持 len()
python
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
def __len__(self):
return len(self.items)
if __name__ == '__main__':
cart = ShoppingCart()
cart.add("矿泉水")
cart.add("饼干")
print(len(cart))
运行结果:
2
__len__() 应返回非负整数。
对于没有自定义 __bool__() 的对象,Python 也可以根据 __len__() 的结果判断真假:
if cart:
print("购物车有商品")
9.4 eq:定义相等规则
普通自定义类如果不定义相等逻辑,两个分别创建的实例即使属性一样,也通常不会比较为相等。
python
class Product:
def __init__(self, product_id, name):
self.product_id = product_id
self.name = name
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return self.product_id == other.product_id
if __name__ == '__main__':
first = Product(1001, "矿泉水")
second = Product(1001, "矿泉水")
print(first == second)
print(first is second)
运行结果:
True
False
NotImplemented 是一个特殊返回值,表示当前方法不负责这种类型组合的比较。它不是 NotImplementedError 异常。
相等规则应保持稳定、清晰。定义 __eq__ 后,在没有额外设置 __hash__ 的常见情况下,实例会变为不可哈希,不能直接放入集合或作为字典键。不要随意添加哈希逻辑,尤其是在参与比较的字段可以变化时。
10. dataclass:简化数据类
10.1 自动生成常见方法
如果一个类主要用于保存数据,可以使用 dataclass:
python
from dataclasses import dataclass
@dataclass
class ProductInfo:
product_id: int
name: str
stock: int = 0
if __name__ == '__main__':
first = ProductInfo(1001, "矿泉水", 100)
second = ProductInfo(1001, "矿泉水", 100)
print(first)
print(first == second)
运行结果:
ProductInfo(product_id=1001, name='矿泉水', stock=100)
True
在默认设置下,dataclass 会根据字段生成常用方法,包括:
__init__。__repr__。__eq__。
默认相等比较要求类型相同,再按字段比较,不等同于前面"只比较商品编号"的自定义规则。
10.2 类型注解不等于运行时校验
ProductInfo(1001, "矿泉水", "很多")
不会仅因为 stock: int 就自动报错。
如果需要初始化校验,可以使用 __post_init__():
python
from dataclasses import dataclass
@dataclass
class ProductInfo:
product_id: int
name: str
stock: int = 0
def __post_init__(self):
if type(self.stock) is not int or self.stock < 0:
raise ValueError("库存必须是非负整数")
if __name__ == '__main__':
try:
ProductInfo(1001, "矿泉水", -1)
except ValueError as error:
print(error)
运行结果:
库存必须是非负整数
注意,这只校验初始化过程。之后直接执行:
product.stock = -1
不会自动再次运行 __post_init__。需要持续控制属性变化时,应考虑 property、明确的方法接口或其他设计。
10.3 可变默认值使用 default_factory
python
from dataclasses import dataclass, field
@dataclass
class ShoppingCart:
items: list[str] = field(default_factory=list)
if __name__ == '__main__':
first = ShoppingCart()
second = ShoppingCart()
first.items.append("矿泉水")
print(first.items)
print(second.items)
运行结果:
['矿泉水']
[]
default_factory=list 会在需要默认值时,为每个实例创建一个新的列表
10.4 frozen=True
如果希望禁止常规字段赋值,可以使用:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
if __name__ == '__main__':
point = Point(10, 20)
print(point)
运行结果:
Point(x=10, y=20)
之后直接执行:
point.x = 30
会抛出 FrozenInstanceError。但它不是递归冻结。如果字段引用了一个列表,该列表内部仍然可能被修改。因此,不要把 frozen=True 理解为所有嵌套数据都不可变。