从抽象类继承SQLAlchemy类时,抛出异常:元类冲突:派生类的元类必须是

9
以下代码是一个非常简单的SqlAlchemy ORM实现,其中包含一个简单的表。Mytable类试图从BaseAbstract继承。
代码抛出以下异常:
Message: metaclass冲突:派生类的元类必须是其所有基类的元类(非严格)子类
from abc import ABC
from sqlalchemy import Column, Integer, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

class BaseAbstract(ABC):
    """description of class"""

SQLALCHEMY_DATABASE_URI =\
   'mssql+pyodbc://(local)/TestDB?driver=SQL+Server+Native+Client+11.0'
SQLALCHEMY_TRACK_MODIFICATIONS = False

engine = create_engine(SQLALCHEMY_DATABASE_URI, echo=True)
Session = sessionmaker(bind=engine)
session = Session()

Base = declarative_base()
metadata = Base.metadata

class Mytable(Base, BaseAbstract):
    __tablename__ = 'myTable'

    id = Column(Integer, primary_key=True)
    firstNum = Column(Integer, nullable=False)
    secondNum = Column(Integer, nullable=False)

如果您将类声明行更改为
class Mytable(Base):
代码将正常工作。另外,如果您将 class BaseAbstract(ABC): 更改为 class BaseAbstract(object): ,代码也会正常工作。如何从SQLAlchemy的抽象类继承?
4个回答

12

混合元类并不容易, 你应该避免它。SQLAlchemy提供了一种处理抽象基类扩展基类的方法,而另一方面,你试图做的看起来很像一个mixin

你可以使用__abstract__指示SQLAlchemy跳过为类创建表和映射器:

Base = declarative_base()

class BaseAbstract(Base):
    """description of class"""
    __abstract__ = True

class Mytable(BaseAbstract):
    ...

您还可以增强 Base

class BaseAbstract:
    """description of class"""

Base = declarative_base(cls=BaseAbstract)

class Mytable(Base):
    ...

但是我认为最简单的解决方案是完全放弃使用"抽象基类",并将其视为mixin,就像您已经做过的一样:

class CommonMixin:
    """description of class"""

Base = declarative_base()

class Mytable(CommonMixin, Base):
    ...

但是如果您坚持使用实际的 abc.ABC 抽象基类,register 您的模型类作为虚拟子类:

class BaseAbstract(ABC):
    """description of class"""

Base = declarative_base()

@BaseAbstract.register
class Mytable(Base):
    ...

这样做的缺点是无法在实例化虚拟子类时检查使用了@abc.abstractmethod修饰的方法。
如果上述方法不能满足您的需求,您可以尝试按照异常提示所示创建一个新的元类,该元类是DeclarativeMetaABCMeta的组合。
In [6]: class DeclarativeABCMeta(DeclarativeMeta, abc.ABCMeta):
   ...:     pass
   ...: 

In [7]: Base = declarative_base(metaclass=DeclarativeABCMeta)

In [8]: class BaseAbstract(abc.ABC):
   ...:     @abc.abstractmethod
   ...:     def foo(self):
   ...:         pass
   ...:     

In [13]: class MyTable(Base, BaseAbstract):
    ...:     __tablename__ = 'mytable'
    ...:     id = Column(Integer, primary_key=True)
    ...:     

In [14]: MyTable()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-1686a36a17c6> in <module>()
----> 1 MyTable()

TypeError: "Can't instantiate abstract class MyTable with abstract methods foo"

In [18]: class MyOtherTable(Base, BaseAbstract):
    ...:     __tablename__ = 'myothertable'
    ...:     id = Column(Integer, primary_key=True)
    ...:     def foo(self):
    ...:         return 'bar'
    ...:     

In [19]: MyOtherTable()
Out[19]: <__main__.MyOtherTable at 0x7f01b4b592b0>

我不能为此担保,不过可能会有更多惊喜。


我尝试从继承ABC的抽象类中继承的原因是为了在一个函数中使用isinstance,该函数以对象作为参数,如果返回true,则通过调用抽象类中定义的方法进行额外处理。如果我没有从ABC继承,Python就不会强制执行我的需求,即确保继承类实现抽象类的所有属性,如果我在实现中遗漏了一些内容,则会得到运行时异常。我宁愿得到构建异常。这就是ABC给我的东西。这难道不是使用ABC的好理由吗? - Barka
我认为通常的答案是你的测试应该捕获一个类是否没有实现所需的方法。我不确定,如果我错了,请纠正我,但我不认为abstractmethod检查调用签名,例如。 - Ilja Everilä
你是正确的。我来自C#/Java背景。那是我们过去的做法,现在我会继续运用这些习惯(好的和不好的)。我仍然喜欢依靠抽象方法(如果Python有接口也会使用),以防忘记实现测试。应用程序完整性执行的方式越多越好。请告诉我是否我的推理错误,我没有按照Pythonic方式进行操作。 - Barka
我同意你的推理,并且有时会想念Python中可强制执行的接口,特别是因为我是一个懒惰的测试编写者。只是我有点担心混合元类,因为它们可能以令人惊讶的方式相互操作。顺便说一下,你是否遇到过zope.interface?他们与ABCs有一些重叠 - Ilja Everilä
我并不是很喜欢Zope的语法。它太复杂了。我更喜欢使用Python抽象类。 - Barka

1
我遇到了同样的问题。除了Ilja提供的非常好的答案之外,因为在他们的回答中有评论询问,所以我想解释一下。
引用: “我试图从继承ABC的抽象类继承是为了在一个函数中使用isinstance来接受一个对象作为参数,如果返回true,则通过调用在抽象类中定义的方法进行额外处理。如果我不继承ABC,Python将不会强制我确保继承类实现抽象类的所有属性,如果我在实现中错过了某些属性,那么我将得到运行时异常。我宁愿得到构建异常。这就是ABC给我的。这不是使用ABC的好理由吗?”
我想分享我在来这里之前创建的解决方法。除了ABC之外,Python提供了另一种检查类实例化期间某个方法是否存在的方法,即在__init_subclass__魔术方法中。我做了以下操作:
class BaseAbstract:
    ABSTRACT_METHODS = [
        "some_method", "another_method"
    ]

    def __init_subclass__(cls):
        for method_name in BaseAbstract.ABSTRACT_METHODS:
            method = getattr(cls, method_name, None)
            if not method or not callable(method):
                raise TypeError(
                    f"Can't instantiate abstract class {cls.__name__} with "
                    f"abstract methods {method_name}."
                )

使用 inspect,您还可以检查“抽象”方法签名的约束条件。使用这种方法是否比使用Ilja的想法更好,这是一个有争议的问题。我只是想分享我的方法,而不是声称它更优越。
我看到我的解决方法有两个不错的特点:
  1. 我只有一个SQLalchemy Base。因此,我不必担心多个Bases,例如在使用alembic的自动生成迁移功能或调用Base.metadata.create_all()时。

  2. 实际上,我不必理解问题是什么。如果您从未处理过元数据冲突,则使用我的方法更容易理解为什么它可以解决问题。

但仍然可能被认为是某种hacky。它还具有在类初始化期间检查方法存在的特征,而不是在实例化期间进行检查。这是一个限制,在级联继承中,所有方法都存在于最后一个子类上,但不在继承链中的所有中间类上。

0

你可以通过为你的Mytable引入一个中间基类来解决这个问题。例如:

Base = declarative_base()
metadata = Base.metadata

class IntermediateBase(type(Base), BaseAbstract):
    pass

class Mytable(IntermediateBase):
    __tablename__ = 'myTable'

    id = Column(Integer, primary_key=True)
    firstNum = Column(Integer, nullable=False)
    secondNum = Column(Integer, nullable=False)

1
我很喜欢这个。不幸的是,我遇到了这个错误:“在分配'名称'之前无法编译列对象。”正在努力找出原因。 - Barka

0

跟随Ilja Everilä上面的一个例子对我非常有用。混合元类可能会很棘手,但我还没有注意到任何问题。实际上,我将declarative_base子类化为我的父类,其中可以定义常见的函数、属性和抽象方法

from abc import abstractmethod, ABCMeta
from typing import Dict, Any

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta


class DeclarativeABCMeta(DeclarativeMeta, ABCMeta):
    """
    AbstractBaseClass Metaclass

    This allows us to enforce the @abstractmethods defined on MLRegistryBase
    on the underlying sub-classes (Database Tables)
    """
    pass


Base = declarative_base(metaclass=DeclarativeABCMeta)


class TableAbstractParentClass(Base):
    """
    Helper Class for Database Model Inheritance
    """

    # Inform SQLAlchemy This in an AbstractBaseClass Model
    __abstract__ = True

    @classmethod
    @abstractmethod
    def __tablename__(cls) -> str:
        """
        Every Database Table Needs a Name
        """
        pass

    @abstractmethod
    def to_dict(self) -> Dict[str, Any]:
        """
        Example Required Method
        """
        pass


class ExampleTable(TableAbstractParentClass):
    """
    Example Table
    """
    __tablename__ = "example_table"

    id = Column(Integer, unique=True, autoincrement=True,
                primary_key=True, index=True)
    name = Column(String(256), nullable=False)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "name": self.name
        }


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接