Day 23 - 字符串表示 strdel

对象的字符串表示概述

在 Python 中,每个对象都可以被转换为字符串。当你使用 print() 函数打印一个对象,或者在某些需要字符串的地方使用对象时,Python 需要将对象转换为字符串。默认的实现通常显示的是类似 <__main__.ClassName object at 0x7f8a9c123456> 这样的信息,虽然包含了对象的内存地址,但对于人类阅读来说并不友好。

为了提供更有意义的字符串表示,Python 提供了两个魔术方法:__str____repr__。通过在类中重写这两个方法,你可以自定义对象的字符串表现形式,使输出更加友好和有意义。

这两个方法的区别在于:__str__ 是面向用户的字符串表示,用于给终端用户查看;而 __repr__ 是面向开发者的字符串表示,应该尽可能提供详细的信息,以便开发者调试。

str 方法详解

__str__ 方法在以下情况会被调用:

  1. 使用 print() 函数打印对象时
  2. 使用 str() 函数转换对象时
  3. 在需要字符串的地方使用 f-string 格式化对象时
  4. 用户友好的错误消息中

__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__ 方法在以下情况会被调用:

  1. 在交互式解释器中直接输入对象并按回车时
  2. 在调试时显示对象信息
  3. 在某些日志输出中
  4. 如果没有定义 __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__ 方法存在一些潜在问题:

  1. 调用时机不确定:由于 Python 的垃圾回收机制,__del__ 的调用时机可能与预期不符。

  2. 循环引用问题:如果对象之间存在循环引用,__del__ 可能不会被立即调用。

  3. 异常风险:如果在 __del__ 执行期间发生异常,Python 会打印警告但不抛出异常。

  4. 模块级变量问题:当程序退出时,某些 __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 不为空)

hasheq

如果一个类定义了 __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 中与对象字符串表示和生命周期相关的重要概念:

  1. __str__:用户友好的字符串表示,用于 print()str() 函数
  2. __repr__:开发者友好的字符串表示,应能表示重建对象所需的信息
  3. __del__:析构器方法,在对象销毁前调用,但应该尽量避免使用
  4. __bool__:定义对象的布尔值
  5. __hash____eq__:定义对象的哈希值和相等性比较

最佳实践建议:

  • 同时定义 __repr____str__ 以提供不同层级的信息
  • 避免在 __del__ 中抛出异常或执行复杂操作
  • 对于资源清理,使用上下文管理器(with 语句)比 __del__ 更安全可靠
  • 使用 @contextmanager 装饰器可以更简洁地创建上下文管理器

在下一节中,我们将学习私有属性和封装(数据隐藏)的概念。