如何在Flask SQLAlchemy的Player模型中直接访问嵌套属性?
问题:Flask-SQLAlchemy模型属性代理实现(避免递归错误)
我有一个名为Player的Flask模型,代码如下:
class Player: id = Column(Integer, primary_key=True) kills = Column(Integer) bot_id = Column(Integer,ForeignKey("bots.id")) bot = relationship("Bot",back_populates="player") user_id = Column(Integer,ForeignKey("users.id")) user = relationship("User",back_populates="player") player_type = Column(String) # BOT or USER
Bot和User模型各自拥有独立属性,由于前端依赖,我希望直接访问这些属性而非通过关联属性,例如用player.screen_name替代player.bot.screen_name或player.user.screen_name。
我可以通过以下代码实现,但因属性众多,不想逐个编写:
@property def screen_name(self): if self.player_type == "BOT": return self.bot.screen_name elif self.player_type == "USER": return self.user.screen_name else: return None
于是我尝试了以下代码:
def __getattr__(self, attr): if hasattr(self, attr): return getattr(self, attr) elif self.player_type == "BOT" and hasattr(self.bot, attr): return getattr(self.bot, attr) elif self.player_type == "USER" and hasattr(self.user, attr): return getattr(self.user, attr) else: raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'")
但该代码因getattr导致递归错误,请问有没有其他实现方式?
解决方案
递归错误原因
__getattr__仅在实例找不到指定属性时才会被调用,而你代码里的hasattr(self, attr)会触发getattr检查,当属性不存在时,又会递归调用__getattr__,形成无限循环。
方法1:改进__getattr__避免递归
直接通过实例的__dict__和SQLAlchemy的模型映射器检查属性,跳过hasattr的触发:
def __getattr__(self, attr): # 检查实例自身已加载的属性或模型定义的列 if attr in self.__dict__ or attr in self.__mapper__.columns: return object.__getattribute__(self, attr) # 根据player_type获取目标关联模型 target = self.bot if self.player_type == "BOT" else self.user # 检查目标模型是否存在该属性 if target and hasattr(target, attr): return getattr(target, attr) # 抛出标准属性错误 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'")
注意:添加target的非空判断,避免关联未加载时引发异常;self.__mapper__.columns用来检测模型定义的所有列属性。
方法2:批量生成属性代理
如果不想用__getattr__全局转发,可批量生成@property代理,只代理指定属性:
def proxy_property(attr_name): @property def prop(self): if self.player_type == "BOT": return getattr(self.bot, attr_name) if self.bot else None elif self.player_type == "USER": return getattr(self.user, attr_name) if self.user else None return None return prop class Player: # 原有模型定义... id = Column(Integer, primary_key=True) kills = Column(Integer) bot_id = Column(Integer,ForeignKey("bots.id")) bot = relationship("Bot",back_populates="player") user_id = Column(Integer,ForeignKey("users.id")) user = relationship("User",back_populates="player") player_type = Column(String) # BOT or USER # 列出需要代理的属性列表 proxy_attrs = ["screen_name", "level", "avatar_url"] # 批量给Player类添加代理属性 for attr in proxy_attrs: setattr(Player, attr, proxy_property(attr))
这种方式更可控,避免意外转发无关属性。
方法3:SQLAlchemy混合属性(支持查询场景)
如果需要在SQL查询中也能使用这些代理属性,用hybrid_property实现实例访问和SQL表达式的统一:
from sqlalchemy.ext.hybrid import hybrid_property class Player: # 原有模型定义... id = Column(Integer, primary_key=True) kills = Column(Integer) bot_id = Column(Integer,ForeignKey("bots.id")) bot = relationship("Bot",back_populates="player") user_id = Column(Integer,ForeignKey("users.id")) user = relationship("User",back_populates="player") player_type = Column(String) # BOT or USER @hybrid_property def screen_name(self): if self.player_type == "BOT": return self.bot.screen_name if self.bot else None elif self.player_type == "USER": return self.user.screen_name if self.user else None return None @screen_name.expression def screen_name(cls): from sqlalchemy import case # 编写SQL层面的条件映射 return case( [(cls.player_type == "BOT", Bot.screen_name)], else_=User.screen_name )
这种方式既支持实例直接访问,也能在Player.query.filter(Player.screen_name == "xxx")这类查询中生效。
内容的提问来源于stack exchange,提问作者Vikas Sharma
相关产品推荐
相关产品推荐

