Python @property装饰器getter/setter返回空列表问题求解
问题说明
你在编写API数据采集脚本时,尝试使用@property装饰器实现的getter/setter来追加存储从API获取的数据到列表中,但直接访问列表始终为空,参考大量示例仍未找到正确实现方式。
如果取消注释示例中的aaa方法并打印,可以得到正确结果,直接调用原sub_categories方法也能正常工作,但不希望通过aaa这类额外包装函数来调用逻辑。
注:目前仅掌握函数相关知识,从未接触过类与装饰器,这是第一个OOP项目,希望学习符合OOP规范的正确实现方式。
项目目标:拉取API数据、完成数据清洗、生成pandas DataFrame后持久化存储。
原问题代码
import requests from typing import Dict, List headers_ = { 'sm-token': '{"Location":{"Latitude":-23.551548099999998,"Longitude":-46.6331008},"IdClientAddress":0,"IsDelivery":false,"IdLoja":2691,"IdRede":884,"DateBuild":"2022-06-08T00:09:36.738357"}', } class Scraper: def __init__(self): self.session = requests.Session() self.header = {'sm-token': '{"Location":{"Latitude":-23.551548099999998,"Longitude":-46.6331008},' '"IdClientAddress":0,"IsDelivery":false,"IdLoja":2691,"IdRede":884,"DateBuild":' '"2022-06-08T00:09:36.738357"}'} self.category_list = ['bebidas', 'bebidas-alcoolicas'] self.market_id = [2691] self.sub_categ_list = [] def sub_categories(self) -> List: url_sub_categ = (f'https://b2c-api-premiumlabel-production.azurewebsites.net/api/b2c/page/' f'menu?id_loja={self.market_id[0]}') for dep in self.session.get(url_sub_categ, headers=self.header).json()['departments']: self.sub_categ_list.append((dep['imagem_url'], dep['categories'])) @property def sub_categ_list(self): print('getter') return self._sub_categ_list @sub_categ_list.setter def sub_categ_list(self, valor): valor = [str(x).replace('4', '40') for x in valor] print('setter') self._sub_categ_list = valor scrape = Scraper() print(scrape.sub_categ_list)
问题根因
- 实例化类之后你从未调用过
sub_categories()方法,初始化阶段仅给sub_categ_list赋值了空列表,根本没触发API请求和数据追加逻辑,直接访问必然是空值。 - setter触发逻辑理解错误:setter仅在对属性整体赋值时才会执行,对列表调用
.append()属于原地修改列表对象,只会触发getter拿到现有列表,不会走你写的替换4为40的清洗逻辑。 - 初始化时
self.sub_categ_list = []会直接触发setter,把空列表赋值给内部存储变量self._sub_categ_list,后续不主动调用拉取方法的话,这个列表永远是空的。
符合OOP规范的修正方案
用property做懒加载是最合适的方案:第一次访问属性时自动拉取、清洗、缓存数据,后续访问直接返回缓存结果,不需要额外写包装方法,也不用手动调用拉取逻辑。
import requests from typing import List, Tuple import pandas as pd class Scraper: def __init__(self, market_id: int = 2691): # 初始化公共配置,内部缓存变量用单下划线开头标识为私有属性 self.session = requests.Session() self.header = { 'sm-token': '{"Location":{"Latitude":-23.551548099999998,"Longitude":-46.6331008},' '"IdClientAddress":0,"IsDelivery":false,"IdLoja":2691,"IdRede":884,"DateBuild":' '"2022-06-08T00:09:36.738357"}' } self.category_list = ['bebidas', 'bebidas-alcoolicas'] self.market_id = market_id # 缓存初始为None,标记为未拉取数据 self._sub_categ_list = None def _fetch_sub_categories(self) -> List[Tuple[str, dict]]: # 内部私有方法:仅负责拉取API原始数据,不对外暴露 url_sub_categ = (f'https://b2c-api-premiumlabel-production.azurewebsites.net/api/b2c/page/' f'menu?id_loja={self.market_id}') resp = self.session.get(url_sub_categ, headers=self.header) resp.raise_for_status() # 请求失败直接抛错,避免无感知的异常 raw_data = [] for dep in resp.json()['departments']: raw_data.append((dep['imagem_url'], dep['categories'])) return raw_data def _clean_data(self, raw_data: List[Tuple[str, dict]]) -> List[Tuple[str, dict]]: # 内部私有方法:仅负责数据清洗逻辑 cleaned_data = [] for img_url, categ_info in raw_data: cleaned_img_url = str(img_url).replace('4', '40') cleaned_categ = str(categ_info).replace('4', '40') cleaned_data.append((cleaned_img_url, cleaned_categ)) return cleaned_data @property def sub_categ_list(self): # getter实现懒加载:首次访问自动拉取、清洗、缓存,后续访问直接返回缓存 if self._sub_categ_list is None: print("首次访问,开始拉取并清洗数据") raw_data = self._fetch_sub_categories() self._sub_categ_list = self._clean_data(raw_data) print("访问子分类列表") return self._sub_categ_list def to_dataframe(self) -> pd.DataFrame: # 单一职责:专门负责转pandas DataFrame return pd.DataFrame(self.sub_categ_list, columns=['imagem_url', 'categories']) def save(self, save_path: str = "sub_categories.csv"): # 单一职责:专门负责持久化存储 self.to_dataframe().to_csv(save_path, index=False, encoding='utf-8-sig') if __name__ == "__main__": scrape = Scraper() # 直接访问属性即可,无需手动调用拉取方法 print(scrape.sub_categ_list) # 后续直接调用存储方法即可完成全流程 # scrape.save()
关键设计说明
- 私有成员规范:内部使用的拉取、清洗方法加单下划线前缀
_,标识这是类内部逻辑,不需要外部直接调用。 - 懒加载逻辑:第一次访问
sub_categ_list时才会发请求拉数据,之后访问直接走缓存,不会重复请求接口,也不需要额外写包装函数。 - 职责拆分:把拉取、清洗、转DataFrame、存储拆成独立方法,后续修改某部分逻辑不会影响其他模块,符合OOP单一职责原则。
- 默认只读:没有写setter,
sub_categ_list默认是只读属性,避免外部误操作覆盖数据,如果需要支持外部赋值,再补充setter逻辑即可。 - 异常兜底:加了
resp.raise_for_status(),接口返回4xx/5xx错误时会直接抛出明确异常,不会出现请求失败后遍历空数据的无意义报错。 - 复用性优化:去掉了冗余的全局变量,实例化时可以传入不同的
market_id拉不同门店的数据,不需要修改类内部代码。
内容的提问来源于stack exchange,提问作者gimmedata
相关产品推荐
相关产品推荐

