13

我正在尝试在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 人显然知道他们在做什么,我认为我完全走错了轨道。有人可以解释这里发生了什么吗?

4

3 回答 3

17

第一件事。需要返回。__init___ Python 文档说“不能返回任何值”,但在 Python 中“放弃”函数的末尾而不点击 return 语句相当于. 因此,显式返回(作为文字或通过返回导致 的表达式的值)也没有害处。Nonereturn NoneNoneNone

所以你引用的__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__属性并不是魔法,它实际上只是一个巨大的黑客来传达信息“我希望这个类块通过反向通道创建这个元类的实例而不是”的实例,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)

于 2012-10-02T22:55:53.863 回答
11

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

于 2012-10-04T14:31:21.023 回答
6
  • print result在我自己的课堂上总是打印无。

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

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

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

当我对 进行子类化DeclarativeMeta时,实际上我所做的一切都是__init__按照 SQLAlchemy 的代码进行的。在阅读您的问题后回想起来,我的代码应该__new__改用。

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

我认为本解释得很好。无论如何,如果您愿意(不推荐),您可以跳过调用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.

于 2012-10-02T23:32:21.600 回答