SqlAlchemy元类混淆

时间:2012-09-30 19:35:22

标签: python sqlalchemy metaclass

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

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人员显然知道他们在做什么,我认为我处于完全错误的轨道上。有人可以解释一下这里发生了什么吗?

3 个答案:

答案 0 :(得分:15)

首先要做的事情。 __init__ 必需才能返回NonePython docs说“没有值可以被返回”,但是在Python中“退出函数的末尾”而没有命中return语句相当于return None。因此,显式返回None(作为文字或通过返回导致None的表达式的值)也不会造成任何伤害。

所以你引用的__init__ DeclarativeMeta方法看起来有些奇怪,但它没有做错任何事。在这里,我再次添加了一些评论:

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__属性不是什么魔法,它实际上只是一个巨大的黑客传播信息“我希望这个类块创建这个元类的实例而不是{{1的实例“通过反向通道,因为没有适当的通道将该信息传递给解释器。 1

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

type

这是执行以下 2 的粗略语法糖:

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

所以“具有属性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_) 的类具有[元类]作为值” IS 元类的实例!它们完全是一回事。唯一的区别是,如果直接创建类(通过调用元类)而不是使用类块和__metaclass__属性,那么它不一定具有__metaclass__作为属性。 3

最后调用__metaclass__就像任何其他类调用一样。它将调用metaclass来创建类对象,然后在结果对象上调用metaclass.__new__(classname, bases, dict_)来初始化它。

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

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


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

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

3 即使是具有元类的类(metaclass除外)也不是通常的方式来创建type属性;检查类的正确方法与检查其他任何类的方法相同;使用__metaclass__,或应用cls.__class__

答案 1 :(得分:10)

SQLAlchemy版本中的__init__基本上是错误的。它可能是三年前通过从某个地方剪切和粘贴元类来编写的,或者它可能是从一个不同的方法开始的,后来成为__init__,并且刚刚没有改变。我刚刚在第一次写入时检查了0.5,看起来大致相同,带有不必要的“返回”语句。现在解决它,很抱歉让你感到困惑。

答案 2 :(得分:6)

    我自己班级中的
  • print result始终打印无。

这是因为构造函数不返回任何内容:)

  • 为什么元类使用__init__而非__new__

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

当我继承DeclarativeMeta时,我实际上在SQLAlchemy的代码之后在__init__中完成了所有操作。回顾完阅读问题后,我的代码应该使用__new__代替。

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

我认为Ben很好地解释了这一点。无论如何,如果你想要(不推荐),你可以跳过调用declarative_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中定义的内容的自动完成。