SqlAlchemy 元类混淆

Posted

技术标签:

【中文标题】SqlAlchemy 元类混淆【英文标题】:SqlAlchemy metaclass confusion 【发布时间】:2012-09-21 18:39:31 【问题描述】:

我正在尝试在SqlAlchemy的类构建过程中注入一些自己的代码。试图理解代码,我对元类的实现有些困惑。以下是相关的sn-ps:

SqlAlchemy 的默认“元类”:

class DeclarativeMeta(type):
    def __init__(cls, classname, bases, dict_):
        if '_decl_class_registry' in cls.__dict__:
            return type.__init__(cls, classname, bases, dict_)
        else:
            _as_declarative(cls, classname, cls.__dict__)
        return type.__init__(cls, classname, bases, dict_)

    def __setattr__(cls, key, value):
        _add_attribute(cls, key, value)

declarative_base 是这样实现的:

def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
                     name='Base', constructor=_declarative_constructor,
                     class_registry=None,
                     metaclass=DeclarativeMeta):
     # some code which should not matter here
     return metaclass(name, bases, class_dict)

它是这样使用的:

Base = declarative_base()

class SomeModel(Base):
    pass

现在我已经像这样派生了自己的元类:

class MyDeclarativeMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        result = DeclarativeMeta.__init__(cls, classname, bases, dict_)
        print result
        # here I would add my custom code, which does not work
        return result

并像这样使用它:

Base = declarative_base(metaclass=MyDeclarativeMeta)

好的,现在解决我的问题:

print result 在我自己的班级中总是打印 None。 代码似乎仍然有效!? 为什么元类使用__init__ 而不是__new__ declarative_base 返回此类的一个实例。它不应该返回一个具有__metaclass__ 属性的类,并将MyDeclarativeMeta 作为值吗?

所以我想知道为什么代码完全有效。由于 SqlAlchemy 人显然知道他们在做什么,我认为我完全走错了轨道。有人能解释一下这里发生了什么吗?

【问题讨论】:

【参考方案1】:

SQLAlchemy 版本中的__init__ 基本上是错误的。它可能是三年前通过从某个地方剪切和粘贴一个元类来编写的,或者它可能是作为一种不同的方法开始的,后来变成了__init__,只是没有改变。我刚在第一次编写时检查了 0.5,它看起来基本相同,但带有不必要的“return”语句。现在正在修复它,抱歉让您感到困惑。

【讨论】:

【参考方案2】: print result 在我自己的班级中总是打印无。

这是因为构造函数没有返回任何东西:)

为什么元类使用__init__ 而不是__new__

我认为这是因为 SQLAlchemy 需要将 cls 的引用存储到声明性类注册表中。在__new__ 中,该类还不存在(参见https://***.com/a/1840466)。

当我对DeclarativeMeta 进行子类化时,我实际上在__init__ 中按照 SQLAlchemy 的代码进行了所有操作。阅读您的问题后回想起来,我的代码应该改用__new__

declarative_base 返回此类的一个实例。它不应该返回一个具有__metaclass__ 属性的类,并将MyDeclarativeMeta 作为值吗?

我认为 Ben 解释得很好。无论如何,如果您愿意(不推荐),您可以跳过调用 declarative_base() 并创建自己的 Base 类,例如

# Almost the same as:
#   Base = declarative_base(cls=Entity, name='Base', metaclass=MyDeclarativeMeta)
# minus the _declarative_constructor.
class Base(Entity):
    __metaclass__ = MyDeclarativeMeta

    _decl_class_registry = dict()
    metadata = MetaData()

在这种情况下,__metaclass__ 属性将在那里。实际上,我创建了这样的 Base 类来帮助 PyCharm 自动完成 Entity 中定义的内容。

【讨论】:

【参考方案3】:

首先要做的事情。 __init__必须返回None。 Python docs 说“没有值可能被返回”,但在 Python 中“丢弃函数的末尾”而不命中 return 语句等同于 return None。因此,显式返回 None(作为文字或通过返回导致 None 的表达式的值)也没有害处。

所以你引用的DeclarativeMeta__init__ 方法对我来说有点奇怪,但它并没有做错任何事情。这里又是我添加的一些 cmets:

def __init__(cls, classname, bases, dict_):
    if '_decl_class_registry' in cls.__dict__:
        # return whatever type's (our superclass) __init__ returns
        # __init__ must return None, so this returns None, which is okay
        return type.__init__(cls, classname, bases, dict_)
    else:
        # call _as_declarative without caring about the return value
        _as_declarative(cls, classname, cls.__dict__)
    # then return whatever type's __init__ returns
    return type.__init__(cls, classname, bases, dict_)

这可以更简洁明了地写成:

def __init__(cls, classname, bases, dict_):
    if '_decl_class_registry' not in cls.__dict__:
        _as_declarative(cls, classname, cls.__dict__)
    type.__init__(cls, classname, bases, dict_)

我不知道为什么 SqlAlchemy 开发人员觉得需要返回 type.__init__ 返回的任何内容(仅限于 None)。当__init__ 可能会返回某些东西时,也许它是在证明未来。也许这只是为了与其他方法保持一致,其中核心实现是推迟到超类;通常你会返回超类调用返回的任何东西,除非你想对它进行后处理。但是它肯定并没有真正做任何事情。

所以您的print result 打印None 只是表明一切都按预期工作。


接下来,让我们仔细看看元类的真正含义。元类只是一个类的类。像任何类一样,您可以通过调用元类来创建元类的实例(即类)。类块语法并不是真正创建类的原因,它只是用于定义字典然后将其传递给元类调用以创建类对象的非常方便的语法糖。

__metaclass__ 属性并没有什么神奇之处,它实际上只是一个巨大的黑客来传达信息“我希望这个类块创建这个元类的实例而不是 type 的实例”通过一个反向渠道,因为没有适当的渠道将该信息传达给口译员。1

举个例子可能会更清楚。采取以下类块:

class MyClass(Look, Ma, Multiple, Inheritance):
    __metaclass__ = MyMeta

    CLASS_CONST = 'some value'

    def __init__(self, x):
        self.x = x

    def some_method(self):
        return self.x - 76

这大致是执行以下操作的语法糖2

dict_ = 

dict_['__metaclass__'] = MyMeta
dict_['CLASS_CONST'] = 'some value'

def __init__(self, x):
    self.x = x
dict_['__init__'] = __init__

def some_method(self):
    return self.x - 76
dict_['some_method'] = some_method

metaclass = dict_.get('__metaclass__', type)
bases = (Look, Ma, Multiple, Inheritance)
classname = 'MyClass'

MyClass = metaclass(classname, bases, dict_)

因此,“具有属性__metaclass__ 的类具有[元类] 作为值”元类的实例!它们是完全一样的。唯一的区别是,如果您直接创建类(通过调用元类)而不是使用类块和__metaclass__ 属性,那么它不一定具有__metaclass__ 作为属性。3支持>

最后对metaclass 的调用与任何其他类调用完全相同。它将调用metaclass.__new__(classname, bases, dict_) 来创建类对象,然后在生成的对象上调用__init__ 来初始化它。

默认元类type 只在__new__ 中做任何有趣的事情。我在示例中看到的元类的大多数用途实际上只是实现类装饰器的一种复杂方式。他们想在创建类时进行一些处理,然后不在乎。所以他们使用__new__,因为它允许他们在type.__new__之前和之后执行。最终结果是每个人都认为__new__ 是您在元类中实现的。

但实际上你可以有一个__init__ 方法;它将在创建后在新的类对象上调用。如果您需要为类添加一些属性,或者在某个注册表中记录类对象,这实际上是一个比__new__ 更方便的地方(也是逻辑上正确的地方)。


1 在 Python3 中,这是通过在基类列表中添加 metaclass 作为“关键字参数”而不是作为类的属性来解决的。

2实际上它稍微复杂一些,因为需要在构建的类和所有基础之间实现元类兼容性,但这是核心思想。

3 即使是具有元类(type 除外)的类也不一定必须__metaclass__ 作为属性;检查类的类的正确方法与检查其他任何类的方法相同;使用cls.__class__,或申请type(cls)

【讨论】:

感谢这些详细的解释,这些解释已经澄清了很多。但是一些图片仍然没有到位:所以如果我有“A类(B,C,D)”A的类型不依赖于B,C或D具有元类属性,但在蜜蜂 B、C 或 D 的类型与“类型”不同时。正确的?通过给 A 一个 metaclass 属性,我明确地说:A、B、D 属于“类型”类型,但希望有另一种类型。假设 A 属于 MA 类型,B 属于 MB 类型。 A的类型是如何选择的?在哪里可以找到有关此主题的文档? @Achim 当你设置一个类的元类时,你说你的类是元类的一个实例。继承意味着这也应该适用于新类的任何子类。这意味着类A 的元类必须是每个基类(BCD)的元类的子类。 @Achim 由于type 是所有类的类,当只涉及一个元类时,事情通常“正常工作”。如果A 没有__metaclass__,并且BCD 之一具有一个(或其中几个具有相同的元类),那么A 也将具有相同的元类。当涉及到两个元类时,事情就变得困难了。我相信你必须创建一个新的元类,它可以从所有涉及的元类中多重继承,并使用它。 Python 无法自动确定不同元类的优先级,因此程序员必须明确地做到这一点。 @Achim 我在 Python 官方文档中找不到太多关于此的内容。这里有一个非常:docs.python.org/reference/…。当我有更多时间时,我会尝试更新我的答案。 __init__ 基本上只是一个错误。现在修复它。

以上是关于SqlAlchemy 元类混淆的主要内容,如果未能解决你的问题,请参考以下文章

结合 Qgraphics 和 sqlalchemy 时出现元类错误

在 SQLAlchemy 中创建混合属性的自定义元类

简单子查询的混淆 SQLAlchemy 转换

使用 SQL Alchemy 声明性基础处理元类冲突

如何使用 SQLAlchemy 声明性语法指定关系?

为啥向元类属性闭包组合添加第二个属性会更改第一个属性?