SQLAlchemy 2.0:如何实现基类带默认值的多态数据类模型?
我正在迁移至SQLAlchemy 2.0,并采用带有MappedAsDataclass的新声明式语法。此前我已为模型实现了联合表继承,简化后的代码如下:
from sqlalchemy import ForeignKey, String from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column class Base(MappedAsDataclass, DeclarativeBase): pass class Foo(Base): __tablename__ = "foo" id: Mapped[int] = mapped_column(primary_key=True) type: Mapped[str] = mapped_column(String(50)) foo_value: Mapped[float] = mapped_column(default=78) __mapper_args__ = {"polymorphic_identity": "foo", "polymorphic_on": "type"} class Bar(Foo): __tablename__ = "bar" id: Mapped[int] = mapped_column(ForeignKey("foo.id"), primary_key=True) bar_value: Mapped[float] __mapper_args__ = {"polymorphic_identity": "bar"}
问题的关键在于foo_value的默认值,它导致程序抛出TypeError: non-default argument 'bar_value' follows default argument错误。虽然在单个类中调整字段顺序可消除该错误(但为何会触发此错误?字段顺序本不应重要),但对于继承模型而言这不可行。
我该如何修复或绕过这一限制?是否遗漏了文档中的相关内容?
报错的核心是MappedAsDataclass会将模型类自动转换为Python的dataclass,而Python的dataclass有严格的参数规则:没有默认值的参数不能出现在带有默认值的参数之后。
在继承场景中,子类Bar的构造函数会自动包含父类Foo的所有字段。父类中foo_value带有默认值,子类的bar_value没有默认值,最终生成的构造函数参数顺序会是id, type, foo_value=78, bar_value——这直接违反了Python的参数规则,因此抛出错误。
方案1:给子类字段添加默认值(业务允许的情况下)
如果bar_value可以有默认值,直接为其设置默认值即可:
class Bar(Foo): __tablename__ = "bar" id: Mapped[int] = mapped_column(ForeignKey("foo.id"), primary_key=True) bar_value: Mapped[float] = mapped_column(default=0.0) # 添加默认值 __mapper_args__ = {"polymorphic_identity": "bar"}
方案2:使用kw_only=True标记父类字段
将父类中带默认值的字段标记为关键字参数,这样dataclass会将其放在参数列表的末尾,不会影响子类的无默认值字段:
class Foo(Base): __tablename__ = "foo" id: Mapped[int] = mapped_column(primary_key=True) type: Mapped[str] = mapped_column(String(50)) # 添加kw_only=True foo_value: Mapped[float] = mapped_column(default=78, kw_only=True) __mapper_args__ = {"polymorphic_identity": "foo", "polymorphic_on": "type"}
如果希望父类所有字段都作为关键字参数,可以在父类上设置全局的dataclass参数:
class Foo(Base): __tablename__ = "foo" __dataclass_args__ = {"kw_only": True} # 全局设置关键字参数 id: Mapped[int] = mapped_column(primary_key=True) type: Mapped[str] = mapped_column(String(50)) foo_value: Mapped[float] = mapped_column(default=78) __mapper_args__ = {"polymorphic_identity": "foo", "polymorphic_on": "type"}
方案3:手动控制dataclass的初始化
如果不想依赖自动生成的构造函数,可以手动给模型类添加@dataclass装饰器并指定init=False,然后自定义构造函数:
from dataclasses import dataclass class Base(MappedAsDataclass, DeclarativeBase): pass @dataclass(init=False) class Foo(Base): __tablename__ = "foo" id: Mapped[int] = mapped_column(primary_key=True) type: Mapped[str] = mapped_column(String(50)) foo_value: Mapped[float] = mapped_column(default=78) __mapper_args__ = {"polymorphic_identity": "foo", "polymorphic_on": "type"} def __init__(self, id, type, foo_value=78): self.id = id self.type = type self.foo_value = foo_value @dataclass(init=False) class Bar(Foo): __tablename__ = "bar" id: Mapped[int] = mapped_column(ForeignKey("foo.id"), primary_key=True) bar_value: Mapped[float] __mapper_args__ = {"polymorphic_identity": "bar"} def __init__(self, id, type, bar_value, foo_value=78): super().__init__(id, type, foo_value) self.bar_value = bar_value
这种方式灵活性高,但需要手动维护构造函数,适合复杂场景。
内容的提问来源于stack exchange,提问作者Nikolai Prokoschenko

