如何修复DeferredStr类,使其在运算符两侧无需str()包裹即可正常工作?
解决DeferredStr类在运算符右侧工作异常的问题
问题背景
我需要实现一个DeferredStr类,行为近似str,用于延迟读取某个对象的属性值——该属性初始未赋值,但会在读取DeferredStr实例前完成赋值。现有基于str继承的实现,在实例位于运算符左侧时工作正常,但位于右侧时会返回创建时的初始占位值(如"not this"),导致结果异常。
问题根源
str是不可变类型,继承str的DeferredStr实例在__new__阶段就绑定了初始字符串值。当DeferredStr位于运算符右侧时,调用的是左侧str对象的方法(如str.__add__),这些方法会直接读取DeferredStr实例的底层初始值,而非触发延迟加载逻辑获取真实属性值。
解决方案:改用collections.UserString继承
UserString是str的可变包装类,内部通过data属性存储字符串内容。我们可以利用这一点,在延迟加载时更新data属性,让所有字符串操作自动使用真实值,无需手动包裹str()。同时,通过元类包装方法确保延迟加载逻辑生效,且兼容类型检查工具(MyPy、Pyright)。
修改后的实现代码
from __future__ import annotations from collections import UserString from functools import wraps from inspect import getmembers, isroutine from typing import Any, Callable, Dict, Final, Optional, Self, Tuple, Type, override def _wrapper(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) def method(self: DeferredStr, *args: Any, **kwargs: Any) -> Any: self._load() return func(self.data, *args, **kwargs) return method class _DeferredStrMetaclass(type(UserString)): NON_WRAPPED_METHODS: Final[Tuple[str, ...]] = ( "__class__", "__class_getitem__", "__delattr__", "__dir__", "__doc__", "__getattribute__", "__getstate__", "__init__", "__init_subclass__", "__new__", "__repr__", "__setattr__", "__subclasshook__", "_load", ) def __new__( cls, clsname: str, bases: Tuple[Type[Any], ...], attrs: Dict[str, Any], **kwargs: Any, ) -> Self: # 包装UserString中所有未被覆盖的方法 for attr, value in getmembers(UserString, predicate=isroutine): if ( attr not in attrs and attr not in _DeferredStrMetaclass.NON_WRAPPED_METHODS ): attrs[attr] = _wrapper(value) return super().__new__(cls, clsname, bases, attrs, **kwargs) class DeferredStr(UserString, metaclass=_DeferredStrMetaclass): _instance: object _attributes: Tuple[str, ...] _expect_str: bool _loaded: bool = False def __init__( self, __instance: object, *__attributes: str, __expect_str: bool = True, ) -> None: # 初始化时用空字符串占位,后续会被真实值覆盖 super().__init__("") self._instance = __instance self._attributes = __attributes self._expect_str = __expect_str self._loaded = False def _load(self) -> None: if self._loaded: return value = self._instance for attribute in self._attributes: value = getattr(value, attribute) if isinstance(value, str): self.data = value elif self._expect_str: raise TypeError(f"{repr(self)} is not a str.") else: self.data = repr(value) self._loaded = True @override def __repr__(self) -> str: return "<{name}: {instance}, {attributes} = {value}>".format( name=type(self).__name__, instance=repr(self._instance), attributes=repr(self._attributes), value=repr(self.data if self._loaded else "unloaded"), ) @override def __eq__(self, __value: object) -> bool: if isinstance(__value, DeferredStr): return ( self._instance == __value._instance and self._attributes == __value._attributes and self._loaded == __value._loaded and self.data == __value.data ) self._load() return self.data == __value # 显式重写__str__确保直接返回真实值 @override def __str__(self) -> str: self._load() return self.data
验证代码
class Test: value: Optional[str] = None instance = Test() deferred_str = DeferredStr(instance, "value") # 模拟延迟赋值 instance.value = "1" # 测试运算符左侧 print(deferred_str + "2") # 输出: 12 print("".join(deferred_str)) # 输出: 1 # 测试运算符右侧 print("2" + deferred_str) # 输出: 21 print("".join((deferred_str,))) # 输出: 1
类型检查兼容说明
- 由于
DeferredStr继承自UserString,而UserString实现了str的所有接口,类型检查工具会将其视为与str兼容的类型。 - 若需要严格标注为
str类型,可以在使用时通过typing.cast(str, deferred_str)辅助类型检查,但通常无需额外操作,因为UserString的行为与str一致。
替代方案:继续继承str(不推荐)
如果坚持继承str,需要手动重写所有反向运算符(如__radd__、__rmul__)和序列操作方法(如__iter__、__getitem__),确保这些方法触发延迟加载并返回真实值。但这种方式代码量更大,且因str的不可变性,始终存在底层初始值被意外访问的风险。
内容的提问来源于stack exchange,提问作者MT0
相关产品推荐
相关产品推荐

