如何封装Downloader类的私有可变DownloadItem,以不可变形式公开属性?
解决方案
要实现仅允许Downloader修改DownloadItem,外部只读访问的需求,核心是通过代理模式实现一个真正的不可变包装器,禁止外部对包装后的对象进行赋值操作,同时避免编写大量样板代码。
1. 改进不可变包装器
重写包装器的__setattr__和__delattr__方法,直接拦截所有外部赋值/删除操作并抛出异常,同时确保内部能正确持有底层DownloadItem对象:
class ImmutableWrapper: def __init__(self, obj): # 直接操作__dict__绕过__setattr__,避免初始化时触发异常 self.__dict__['_obj'] = obj def __getattr__(self, name): # 转发属性获取请求到底层对象 return getattr(self._obj, name) def __setattr__(self, name, value): # 禁止任何外部赋值操作 raise AttributeError("无法修改只读的下载项对象") def __delattr__(self, name): # 禁止任何外部删除属性操作 raise AttributeError("无法删除只读下载项的属性") # 可选:转发字符串表示方法,让包装器的打印更友好 def __str__(self): return str(self._obj) def __repr__(self): return repr(self._obj)
2. 调整Downloader的返回逻辑
在get_download_by_id方法中返回包装后的DownloadItem,而不是原始对象:
class Downloader: def __init__(self): # 用字典存储下载项,key为唯一ID,value为原始DownloadItem self._download_queue = {} def download(self, url, start_immediate=False): # 创建原始DownloadItem(仅Downloader可修改) item = DownloadItem(url) unique_id = id(item) # 或用uuid生成更可靠的唯一ID self._download_queue[unique_id] = item if start_immediate: self.start_download(unique_id) return unique_id def start_download(self, unique_id): # 模拟后台线程更新DownloadItem元数据的逻辑 import threading def update_metadata(): item = self._download_queue[unique_id] # 模拟从服务器获取文件大小 item.filesize = 1024 item.status = "downloading" threading.Thread(target=update_metadata).start() def get_download_by_id(self, unique_id): item = self._download_queue.get(unique_id) if item: # 返回包装后的只读对象 return ImmutableWrapper(item) return None
3. 原始DownloadItem类
保持DownloadItem的可变性,仅在Downloader内部修改:
class DownloadItem: def __init__(self, url): self.url = url self.filesize = -1 # 初始未知 self.status = "pending" self.start_time = None # 其他需要的属性... def __repr__(self): return f"<DownloadItem url: {self.url} filesize: {self.filesize}>"
测试验证
# 初始化下载器 downloader = Downloader() # 添加下载任务 unique_id = downloader.download(url='https://path/to/resource', start_immediate=False) print(downloader._download_queue[unique_id]) # <DownloadItem url: https://path/to/resource filesize: -1> # 启动下载(后台更新元数据) downloader.start_download(unique_id) # 等待线程执行 import time time.sleep(0.1) # 获取只读的下载项对象 download_item = downloader.get_download_by_id(unique_id) print(download_item.filesize) # 1024 # 尝试修改属性,触发异常 download_item.filesize = 2 # AttributeError: 无法修改只读的下载项对象
设计思路说明
- 代理模式:
ImmutableWrapper作为DownloadItem的代理,控制外部访问权限,仅允许读取操作,解耦了Downloader和DownloadItem的耦合度。 - 开闭原则:后续扩展
DownloadItem的字段时,无需修改Downloader或ImmutableWrapper的代码,自动支持新属性的只读访问。 - 明确的错误反馈:直接抛出
AttributeError,避免外部误操作导致的属性混淆(如原包装器中赋值仅作用于包装器本身的问题)。
内容的提问来源于stack exchange,提问作者César
相关产品推荐
相关产品推荐

