Day 23 - 字符串表示 str 与 del
对象的字符串表示概述
在 Python 中,每个对象都可以被转换为字符串。当你使用 print() 函数打印一个对象,或者在某些需要字符串的地方使用对象时,Python 需要将对象转换为字符串。默认的实现通常显示的是类似 <__main__.ClassName object at 0x7f8a9c123456> 这样的信息,虽然包含了对象的内存地址,但对于人类阅读来说并不友好。
为了提供更有意义的字符串表示,Python 提供了两个魔术方法:__str__ 和 __repr__。通过在类中重写这两个方法,你可以自定义对象的字符串表现形式,使输出更加友好和有意义。
这两个方法的区别在于:__str__ 是面向用户的字符串表示,用于给终端用户查看;而 __repr__ 是面向开发者的字符串表示,应该尽可能提供详细的信息,以便开发者调试。
str 方法详解
__str__ 方法在以下情况会被调用:
- 使用
print()函数打印对象时 - 使用
str()函数转换对象时 - 在需要字符串的地方使用 f-string 格式化对象时
- 用户友好的错误消息中
__str__ 方法应该返回一个字符串。如果没有定义 __str__,Python 会使用 __repr__ 作为备选。
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
"""返回用户友好的字符串表示"""
return f"《{self.title}》 - {self.author}"
book = Book("活着", "余华", 500)
print(book) # 《活着》 - 余华
print(str(book)) # 《活着》 - 余华
print(f"我正在读{book}") # 我正在读《活着》 - 余华
repr 方法详解
__repr__ 方法在以下情况会被调用:
- 在交互式解释器中直接输入对象并按回车时
- 在调试时显示对象信息
- 在某些日志输出中
- 如果没有定义
__str__,则print()也会调用__repr__
__repr__ 方法应该返回一个字符串,该字符串理论上应该能够用于重新创建对象(即包含足够的重建信息)。如果一个类同时定义了 __str__ 和 __repr__,print() 和 str() 会优先使用 __str__。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
"""返回开发者的字符串表示"""
return f"Point(x={self.x}, y={self.y})"
def __str__(self):
"""返回用户的字符串表示"""
return f"({self.x}, {self.y})"
p = Point(3, 4)
print(repr(p)) # Point(x=3, y=4)
print(str(p)) # (3, 4)
print(p) # (3, 4) -- print 优先使用 __str__
最佳实践:同时定义两个方法
为了提供最佳体验,建议同时定义 __str__ 和 __repr__ 方法。__repr__ 应该提供更详细的信息,让开发者能够精确了解对象的状态;__str__ 则提供更简洁友好的信息。
class Student:
def __init__(self, name, student_id, major, grades=None):
self.name = name
self.student_id = student_id
self.major = major
self.grades = grades if grades else {}
def __repr__(self):
"""开发者友好的表示,包含所有详细信息"""
return (f"Student(name={repr(self.name)}, "
f"student_id={repr(self.student_id)}, "
f"major={repr(self.major)}, "
f"grades={repr(self.grades)})")
def __str__(self):
"""用户友好的表示"""
avg_grade = self.average() if self.grades else 0
return f"{self.name}(学号:{self.student_id},专业:{self.major},平均分:{avg_grade:.1f})"
def add_grade(self, subject, grade):
self.grades[subject] = grade
def average(self):
if not self.grades:
return 0
return sum(self.grades.values()) / len(self.grades)
s = Student("张三", "2023001", "计算机科学与技术")
s.add_grade("数学", 95)
s.add_grade("英语", 88)
s.add_grade("编程", 92)
print(repr(s))
# Student(name='张三', student_id='2023001', major='计算机科学与技术', grades={'数学': 95, '英语': 88, '编程': 92})
print(str(s))
# 张三(学号:2023001,专业:计算机科学与技术,平均分:91.7)
del 析构器方法
__del__ 是 Python 中的析构器方法,当对象即将被销毁时(通常是引用计数降为零或程序退出时)会被调用。它的作用与 __init__ 相反——__init__ 用于初始化对象,而 __del__ 用于清理对象。
需要注意的是,__del__ 并不等同于 C++ 中的析构函数。Python 有自己的垃圾回收机制,__del__ 的调用时机并不完全确定。在大多数情况下,你应该避免使用 __del__,除非确实需要在对象销毁时执行某些清理操作(如关闭文件、释放外部资源等)。
class FileHandler:
def __init__(self, filename):
self.filename = filename
self.file = open(filename, 'w')
print(f"文件 {filename} 已打开")
def write(self, content):
self.file.write(content)
def __del__(self):
"""关闭文件"""
if hasattr(self, 'file') and not self.file.closed:
self.file.close()
print(f"文件 {self.filename} 已关闭")
# 使用上下文管理器(with)更安全,后面的章节会讲到
handler = FileHandler("test.txt")
handler.write("Hello, World!")
# 当 handler 被删除时,__del__ 会被调用
引用计数与垃圾回收
Python 使用引用计数(Reference Counting)作为主要的内存管理机制。当一个对象的引用计数降为零时,对象会立即被销毁,__del__ 方法会被调用。然而,当存在循环引用时,引用计数无法检测到这些循环引用,Python 的垃圾回收器(Garbage Collector)会定期检测并清理这些循环引用的对象。
import gc
class Node:
def __init__(self, value):
self.value = value
self.next = None
def __repr__(self):
return f"Node({self.value})"
def __del__(self):
print(f"Node({self.value}) 被销毁")
# 创建循环引用
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
node3.next = node1 # 形成循环引用:1 -> 2 -> 3 -> 1
print("删除 node1...")
del node1
print("删除 node2...")
del node2
print("删除 node3...")
del node3
# 即使删除了所有引用,循环引用可能导致对象仍未被销毁
# 需要手动触发垃圾回收
print("手动触发垃圾回收...")
gc.collect()
print("完成")
使用 try…finally 确保清理
虽然 __del__ 可以用于清理工作,但更可靠的方式是使用 try...finally 语句或上下文管理器(with 语句)。这样可以确保清理代码在任何情况下都会执行,即使发生异常也不例外。
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connected = False
print(f"连接数据库:{connection_string}")
def connect(self):
self.connected = True
print("数据库连接成功")
def query(self, sql):
if not self.connected:
raise RuntimeError("请先连接数据库")
print(f"执行查询:{sql}")
def close(self):
if self.connected:
self.connected = False
print("数据库连接已关闭")
# 方法1:使用 try...finally
db = DatabaseConnection("mysql://localhost/testdb")
try:
db.connect()
db.query("SELECT * FROM users")
finally:
db.close()
print("---")
# 方法2:使用上下文管理器(推荐)
class DatabaseConnection2:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connected = False
def __enter__(self):
print(f"连接数据库:{self.connection_string}")
self.connected = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.connected:
print("关闭数据库连接")
return False # 不吞没异常
def query(self, sql):
print(f"执行查询:{sql}")
with DatabaseConnection2("mysql://localhost/testdb") as db:
db.query("SELECT * FROM users")
# with 块结束时自动调用 __exit__,确保连接被关闭
为什么应该避免 del
__del__ 方法存在一些潜在问题:
-
调用时机不确定:由于 Python 的垃圾回收机制,
__del__的调用时机可能与预期不符。 -
循环引用问题:如果对象之间存在循环引用,
__del__可能不会被立即调用。 -
异常风险:如果在
__del__执行期间发生异常,Python 会打印警告但不抛出异常。 -
模块级变量问题:当程序退出时,某些
__del__可能根本不会被调用。
import warnings
class Problematic:
def __init__(self, name):
self.name = name
def __del__(self):
# 警告:不要在 __del__ 中抛出异常
raise RuntimeError(f"在 {self.name} 的 __del__ 中发生错误")
# 这会导致警告而非异常
# obj = Problematic("test")
# del obj # 会打印警告但不抛出异常
更好的替代方案:上下文管理器
对于需要清理资源的情况,Python 提供了上下文管理器(Context Manager)作为更优雅、更安全的解决方案。实现上下文管理器有两种方式:定义 __enter__ 和 __exit__ 方法,或者使用 @contextmanager 装饰器。
class FileManager:
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
return False # 不吞没异常
# 使用
with FileManager("example.txt", "w") as f:
f.write("Hello, World!")
# 文件自动关闭,即使写入时发生异常也会正确关闭
使用 @contextmanager 装饰器
contextlib 模块提供了一个 @contextmanager 装饰器,可以更简洁地创建上下文管理器。
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
print(f"获取资源:{name}")
resource = {"name": name, "data": []}
try:
yield resource # 返回要管理的对象
finally:
print(f"释放资源:{name}")
with managed_resource("数据库连接") as res:
res["data"].append("some data")
print(f"使用 {res['name']} 中...")
# 资源自动释放
字符串方法的组合使用
在实际应用中,__str__ 和 __repr__ 通常会与其他字符串方法组合使用,以提供更丰富的功能。
class Vector:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"Vector({self.x}, {self.y}, {self.z})"
def __str__(self):
return f"向量({self.x}, {self.y}, {self.z}),模长:{self.magnitude():.2f}"
def magnitude(self):
return (self.x**2 + self.y**2 + self.z**2) ** 0.5
def __format__(self, format_spec):
"""支持自定义格式化"""
if format_spec == "short":
return f"({self.x}, {self.y}, {self.z})"
elif format_spec == "detailed":
return f"Vector(x={self.x}, y={self.y}, z={self.z}, |v|={self.magnitude():.4f})"
return str(self)
v = Vector(3, 4, 5)
print(repr(v)) # Vector(3, 4, 5)
print(str(v)) # 向量(3, 4, 5),模长:7.07
print(format(v, "short")) # (3, 4, 5)
print(format(v, "detailed")) # Vector(x=3, y=4, z=5, |v|=7.0711)
bool 方法
除了 __str__ 和 __repr__,还有一个相关的魔术方法 __bool__,它用于定义对象的布尔值。在 if 语句或 bool() 函数中需要布尔值时,会调用对象的 __bool__ 方法(如果未定义,则使用 __len__,如果也没有,则默认为 True)。
class EmptyList:
def __init__(self, items=None):
self.items = items if items else []
def __bool__(self):
"""定义对象的布尔值"""
return len(self.items) > 0
lst = EmptyList([])
print(bool(lst)) # False(因为 items 为空)
lst.items.append(1)
print(bool(lst)) # True(因为 items 不为空)
hash 与 eq
如果一个类定义了 __eq__ 方法(用于比较两个对象是否相等),但没有定义 __hash__ 方法,那么这个类的实例将变成不可哈希的(unhashable),不能用作字典的键或集合的元素。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Person(name={self.name}, age={self.age})"
def __eq__(self, other):
if isinstance(other, Person):
return self.name == other.name and self.age == other.age
return False
def __hash__(self):
"""定义哈希值,使得相等的对象有相同的哈希值"""
return hash((self.name, self.age))
p1 = Person("张三", 25)
p2 = Person("张三", 25)
p3 = Person("李四", 25)
print(p1 == p2) # True(内容相等)
print(p1 == p3) # False
print(hash(p1) == hash(p2)) # True(相等的对象哈希值相同)
# 可以用作字典的键或集合的元素
people_set = {p1, p2, p3}
print(len(people_set)) # 2(p1 和 p2 相等,只保留一个)
练习题
练习 1:实现有理数的字符串表示
from math import gcd
class Rational:
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("分母不能为零")
g = gcd(abs(numerator), abs(denominator))
self.numerator = numerator // g
self.denominator = denominator // g
if self.denominator < 0:
self.numerator = -self.numerator
self.denominator = -self.denominator
def __repr__(self):
return f"Rational({self.numerator}, {self.denominator})"
def __str__(self):
if self.denominator == 1:
return str(self.numerator)
return f"{self.numerator}/{self.denominator}"
def __add__(self, other):
if isinstance(other, int):
other = Rational(other)
if not isinstance(other, Rational):
return NotImplemented
num = self.numerator * other.denominator + other.numerator * self.denominator
den = self.denominator * other.denominator
return Rational(num, den)
def __eq__(self, other):
if isinstance(other, int):
other = Rational(other)
if not isinstance(other, Rational):
return False
return self.numerator == other.numerator and self.denominator == other.denominator
# 测试
r1 = Rational(1, 2)
r2 = Rational(2, 4)
r3 = Rational(3)
print(repr(r1)) # Rational(1, 2)
print(str(r1)) # 1/2
print(r1 == r2) # True(约分后相等)
print(r1 + r3) # 7/2
练习 2:实现简单的栈类
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if not self._items:
raise IndexError("栈为空")
return self._items.pop()
def peek(self):
if not self._items:
raise IndexError("栈为空")
return self._items[-1]
def __len__(self):
return len(self._items)
def __bool__(self):
return len(self._items) > 0
def __repr__(self):
return f"Stack({self._items!r})"
def __str__(self):
if not self._items:
return "Stack: [] (空)"
return "Stack: [" + " → ".join(str(x) for x in self._items) + "]"
s = Stack()
print(s) # Stack: [] (空)
print(bool(s)) # False
s.push(1)
s.push(2)
s.push(3)
print(s) # Stack: [1 → 2 → 3]
print(len(s)) # 3
print(s.peek()) # 3
练习 3:实现配置类
class Config:
def __init__(self, **kwargs):
self._settings = {}
for key, value in kwargs.items():
self._settings[key] = value
def __repr__(self):
return f"Config({self._settings!r})"
def __str__(self):
if not self._settings:
return "Config: (空)"
lines = ["Config:"]
for key, value in self._settings.items():
lines.append(f" {key} = {value!r}")
return "\n".join(lines)
def __getitem__(self, key):
return self._settings[key]
def __setitem__(self, key, value):
self._settings[key] = value
def __contains__(self, key):
return key in self._settings
def get(self, key, default=None):
return self._settings.get(key, default)
# 测试
config = Config(
db_host="localhost",
db_port=3306,
db_name="testdb",
debug=True
)
print(str(config))
# Config:
# db_host = 'localhost'
# db_port = 3306
# db_name = 'testdb'
# debug = True
print("db_host" in config) # True
print(config["db_port"]) # 3306
config["timeout"] = 30
print(config.get("nonexistent", "default")) # default
总结
今天我们学习了 Python 中与对象字符串表示和生命周期相关的重要概念:
__str__:用户友好的字符串表示,用于print()和str()函数__repr__:开发者友好的字符串表示,应能表示重建对象所需的信息__del__:析构器方法,在对象销毁前调用,但应该尽量避免使用__bool__:定义对象的布尔值__hash__和__eq__:定义对象的哈希值和相等性比较
最佳实践建议:
- 同时定义
__repr__和__str__以提供不同层级的信息 - 避免在
__del__中抛出异常或执行复杂操作 - 对于资源清理,使用上下文管理器(
with语句)比__del__更安全可靠 - 使用
@contextmanager装饰器可以更简洁地创建上下文管理器
在下一节中,我们将学习私有属性和封装(数据隐藏)的概念。