You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 21:54:29