在Python中,当我们使用print()输出一个自定义类的实例时,默认会看到类似“<__main__.User object at 0x1024e7f80>”这样的内容,它包含对象的内存地址,对使用者几乎没有帮助。想要让对象在被打印时显示有意义的信息,就需要实现__str__方法;而如果希望类的属性访问更加规范、可控,@property装饰器是常用工具。将两者结合,可以让一个类在控制台、日志输出或字符串格式化场景中输出结构清晰、内容准确的文本,同时保持属性封装的优雅性。

一、__str__方法的核心机制与使用场景
__str__是Python数据模型中的特殊方法,当内建str()函数作用于对象、print()输出对象、或者使用f-string、format()格式化对象时,解释器会调用该对象的__str__方法,并将其返回值作为字符串显示。官方约定__str__面向用户,返回易于阅读的文本,而__repr__面向开发者,更偏向准确的、可用于调试的表示。如果类没有定义__str__,但定义了__repr__,str(obj)会退回到调用__repr__;如果都没有定义,就继承object类的默认实现,返回内存地址形式。
实现__str__的语法很简单,在类中定义def __str__(self):返回字符串即可。需要注意的是,__str__的返回值必须是str类型,如果返回整数、None或其他类型,Python解释器会抛出TypeError异常。这一点在拼接自定义字符串时尤其要小心,可以使用f-string或str()转换非字符串值。另外,__str__不应该引发副作用,比如修改属性状态或执行外部I/O,理想情况下它只负责读取状态并构造返回字符串。
对比__repr__:如果在一个交互式环境中直接输入对象名,解释器调用的是__repr__而不是__str__;而print(obj)优先调用__str__。因此很多开发者选择同时实现两个方法,让__repr__返回机器可读或开发者友好的表示,例如包含类名和关键参数,让__str__返回用户友好的描述。例如datetime模块中的datetime对象,__str__返回“2024-05-01 12:00:00”,而__repr__返回“datetime.datetime(2024, 5, 1, 12, 0)”,这种分工值得借鉴。
二、属性装饰器@property如何塑造对象状态
@property是Python内置的装饰器,它可以把类中的一个方法转换成属性访问方式,即通过obj.attr方式调用,而不需要显式加括号。典型用法是先定义一个私有实例变量,比如self._name,然后定义同名方法name并用@property修饰,作为getter;接着可以使用@name.setter定义赋值逻辑,使用@name.deleter定义删除逻辑。这样外部代码在读写name时,实际执行的是方法代码,从而可以加入校验、转换或惰性计算。
属性装饰器的一个显著优势是接口稳定。即使类内部实现从简单属性改为计算属性,外部代码无需调整。例如Circle类可以定义radius为属性,而area作为只读属性,每次访问时根据半径重新计算面积;也可以把temperature属性与华氏度、摄氏度转换结合起来。相比直接暴露公共属性,property提供了更细粒度的控制,同时保持了语法的简洁。对于需要输出自定义字符串的类来说,property定义的属性值可以被__str__直接引用,保证了一致性。
需要注意避免在property的getter或setter中调用自身造成无限递归。例如getter中返回self.name会再次触发property的getter,应返回self._name。此外,property通常只读或读写,如果只定义getter,则外部赋值会引发AttributeError。合理使用property可以让对象的字符串表示基于经过校验的状态,而不是原始的内部变量。
三、结合__str__与@property实现自定义输出
下面以一个订单类为例,展示如何通过@property暴露只读的总价属性和受控的数量属性,再借助__str__输出一段完整的订单摘要。类内部使用_quantity和_unit_price存储私有数据,quantity的setter检查非负数值,total_price根据数量和单价动态计算,不保存冗余状态。__str__方法返回一个多行字符串,包含商品名、数量、单价和总价。这样无论是print(order)还是str(order),都会得到对用户友好的订单信息。
class Order:
def __init__(self, product, quantity, unit_price):
self._product = product
self._quantity = quantity
self._unit_price = unit_price
@property
def product(self):
return self._product
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
if value < 0:
raise ValueError("数量不能为负数")
self._quantity = value
@property
def unit_price(self):
return self._unit_price
@unit_price.setter
def unit_price(self, value):
if value < 0:
raise ValueError("单价不能为负数")
self._unit_price = value
@property
def total_price(self):
return self._quantity * self._unit_price
def __str__(self):
return (f"订单商品:{self.product}\n"
f"数量:{self.quantity}\n"
f"单价:{self.unit_price:.2f}\n"
f"总价:{self.total_price:.2f}")
order = Order("机械键盘", 2, 299.5)
print(order)
order.quantity = 3
print(str(order))
这段代码的运行结果会先输出数量为2的订单信息,然后修改数量为3后输出新的订单。可以看到,__str__内部使用self.product、self.quantity、self.unit_price以及self.total_price这些property属性,而不是直接访问_开头的私有变量。这样即使将来quantity的存储方式改变,比如从简单变量改为数据库查询结果,只要property接口不变,__str__的输出逻辑就无需改动。同时,赋值时如果有负数,会立即抛出ValueError,避免无效状态进入字符串表示。
另一个常见做法是结合__repr__提供开发者视图。__str__返回面向终端用户的描述,而__repr__返回可用于复现对象的字符串,如Order('机械键盘', 2, 299.5)。当调试时,使用repr(order)或直接在交互式环境输入对象,能快速看出构造参数。两者组合可以覆盖日志、调试和界面展示等不同场景。
四、常见误区与注意事项
第一,__str__必须返回字符串。有些开发者习惯在方法中直接print信息并返回None,这会导致print(obj)时额外输出一行None,甚至引发类型错误。正确做法是用return拼接好字符串,由外部决定如何输出。第二,避免在__str__里修改属性状态。__str__只在需要字符串表示时被调用,如果包含自增计数、写入日志文件等副作用,可能在不同上下文中产生难以追踪的状态变化。第三,递归风险:如果__str__中不小心调用str(self)或打印self,会形成无限递归,例如return f"{self}"或return str(self),最终触发RecursionError。
与property结合时,注意不要在getter或setter中发起复杂的IO操作,否则属性访问会变得缓慢,影响__str__的响应速度。如果确实需要昂贵的计算,可以考虑缓存结果,比如使用functools.cached_property(Python 3.8及以上),或者将计算延迟到__str__中一次性完成。另外,property名称不要与私有变量同名,这是导致递归的常见原因。例如定义@property def name(self): return self.name,这会无限递归,必须返回self._name。
在编写类时,可以先用普通属性快速验证业务逻辑,等到需要校验或派生值时再引入property。对于需要自定义输出的类,最小实现就是添加__str__方法;随着类演变,再让__str__引用property。这种渐进式设计既不会过度工程化,也能保证代码可读性。最后,测试时应同时验证str(obj)返回的字符串格式,因为字符串输出往往是API或日志契约的一部分,修改格式会影响下游系统。