我正在尝试在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人员显然知道他们在做什么,我认为我处于完全错误的轨道上。有人可以解释一下这里发生了什么吗?
答案 0 :(得分:15)
首先要做的事情。 __init__
必需才能返回None
。 Python 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
中定义的内容的自动完成。