从元类设置实例变量

Posted

技术标签:

【中文标题】从元类设置实例变量【英文标题】:Set instance variables from metaclass 【发布时间】:2014-02-25 07:50:27 【问题描述】:

给定一个元类,或更简单的type(),最后一个参数代表类字典,它负责类变量。我想知道,有没有办法从元类中设置 instance 变量?

我使用元类的原因是我需要收集一些在创建类时定义的变量并处理它们。但是,此处理的结果需要附加到类的每个实例,而不是类本身,因为结果将是一个列表,该列表会因类的一个实例与另一个实例不同。

我将提供一个示例,以便更容易关注我。


我有一个Model 类,定义如下:

class Model(object):
    __metaclass__ = ModelType

    has_many = None

    def __init__(self, **kwargs):
        self._fields = kwargs

has_many class 变量将在类定义中由某人认为需要的任何模型填充。在__init__() 中,我只是将模型实例化时提供的关键字分配给instance 变量_fields

然后我有 ModelType 元类:

class ModelType(type):
    def __new__(cls, name, bases, dct):
        if 'has_many' in dct:
            dct['_restricted_fields'].append([has-many-model-name])

我需要在这里做的很简单。我检查是否有人在他的自定义模型类上定义了has_many class 变量,然后将该变量的内容添加到受限字段列表中。

现在是重要的部分。实例化模型时,我需要 _fieldsboth 用于实例化模型的 keyword 参数和 restricted fields 组成,如在元类中处理。如果 _fieldsModel 类中的 class 变量,这将非常容易,但由于它是 instance 变量,我不知道我该怎么做还将限制字段添加到其中。


说了这么多,有什么方法可以实现吗?或者有没有更好的方法来处理这个? (我想过只使用一个元类并在模型上设置一个_restricted_fields类变量,然后在类__init__()中使用这个变量将受限字段添加到普通字段中,但是很快模型类就会变得杂乱无章在我看来,代码应该放在代码的另一部分)。

【问题讨论】:

你要找的是这种情况吗:当你创建一个类的新实例时,与现有实例相关的一些变量会发生变化? 其实这是我要防止的。这就是为什么我正在寻找一种在类创建时定义的机制,它将为实例化类时创建的变量分配默认值。 【参考方案1】:

为此使用元类不是正确的方法。 元类修改类创建行为而不是实例创建行为。您应该使用__init____new__函数来修改实例创建行为。想要为此类事情使用元类是使用锤子而不是螺丝刀将螺丝钉在墙上。 ;-)

我建议您使用__new__ 来实现您想要的。来自Python docs:

__new__() 主要用于允许不可变类型的子类(如intstrtuple)自定义实例创建。它通常在自定义元类中被覆盖以自定义类创建。

class MetaModel(type):
    def __new__(cls, name, bases, attrs):
        attrs['_restricted_fields'] = [attrs.get('has_many')]
        return type.__new__(cls, name, bases, attrs)

class Model(object):
    __metaclass__ = MetaModel
    has_many = None
    def __new__(cls, *args, **kwargs):
        instance = object.__new__(cls, *args, **kwargs)
        instance.instance_var = ['spam']
        return instance

class SubModel(Model):
    has_many = True
    def __init__(self):
        # No super call here.
        self.attr = 'eggs'

s = SubModel()
assert s._restricted_fields == [True]  # Added by MetaModel
assert s.instance_var == ['spam']  # Added by Model.__new__
assert s.attr == 'eggs'  # Added by __init__

# instance_var is added per instance.
assert SubModel().instance_var is not SubModel().instance_var

MetaModel 负责创建Model 类。它将_restricted_fields 类变量添加到由它创建的任何Model 类(值是包含has_many 类变量的列表)。

Model 类定义了一个默认的has_many 类变量。它还修改了实例创建行为,为每个创建的实例添加了instance_var 属性。

SubModel 是由您的代码的用户创建的。它定义了一个__init__ 函数来修改实例创建。请注意,它不调用任何超类函数,这不是必需的。 __init__ 为每个 SubClass 实例添加一个 attr 属性。

【讨论】:

我喜欢你的观点。但我使用元类的原因是有很多操作需要完成,并且将它们全部放在Model__new__() 中会使使用Model 类的人看起来更复杂。此外,这些受限字段应该在类创建时计算,而不是每次该类实例化时计算,因为受限字段对于该特定类的每个实例都是相同的.这就是为什么我觉得元类更适合处理受限字段,但是我很难将它们分配给每个实例 @AndreiHorak 我会说在元类中处理类变量,这确实是合适的,因为它涉及类创建。然而,分配应该在__init____new__ 函数中完成。我会更新我的答案,但我无法正确解释这个问题(它实际上不是SSCCE)。 这就是我的想法,但是我应该如何将元类中的已处理变量链接到类'__init__()__new__()?我能想到的唯一方法是在 Model 类上创建一些类变量并在实例化时使用它们,但正如我所说,该类看起来会杂乱无章。 @AndreiHorak Model 类应该获得一个元类(MetaModel 或其他东西)来处理类的变量。 Model 类提供__new__ 函数来修改实例创建。两者看起来都很简单。我将尝试添加到示例中。 听起来很公平。但问题是,这些实例变量正是元类处理的变量。也就是说,_restricted_fields 在您的示例中。这些是我在每个实例中需要的变量。而且从你的例子来看,在元类完成它的工作之后,我在 Model 类上定义了这些变量。但这是我关心的问题,我不喜欢将这些变量存储在 Model 类中,因为它是“后台”的东西,任何使用 Model 类的人都不应该看到。【参考方案2】:

设置实例变量的地方在类的__init__方法中。但是,如果您不想让使用元类的每个类都包含相同的代码,为什么不让元类为该类提供自己的 __init__ 方法,围绕现有的 __init__ 方法:

class MyMetaclass(type):
    def __new__(mcs, name, bases, dct):
        if 'has_many' in dct:
            dct['_restricted_fields'].append(["something"])

        orig_init = dct.get("__init__") # will be None if there was no __init__

        def init_wrapper(self, *args, **kwargs):
            if orig_init:
                orig_init(self, *args, **kwargs)          # call original __init__
            self._fields = getattr(self, "_fields", [])   # make sure _fields exists
            self._fields.extend(["whatever"])             # make our own additions

        dct["__init__"] = init_wrapper   # replace original __init__ with our wrapper

        return type.__new__(mcs, name, bases, dct)

我不完全理解您想要实现的实际逻辑是什么,但您可以将包装函数中的self._fields.extend(["whatever"]) 替换为您真正想要做的任何事情。 self 在这种情况下是实例,而不是类或元类!包装函数是一个闭包,因此您可以访问mcs 中的元类和dct 中的类字典(如果需要)。

【讨论】:

似乎是一种有趣的方法。但考虑到我的Model 类可供任何人使用,代码将有点难以理解。但既然只是添加了一些额外的东西来保留原来的__init__(),我相信它并没有那么糟糕,对吧? 如果您可以依赖使用它的常规类来适当地运行元类(而不是试图做到万无一失),您可以稍微简化一下元类。也就是说,您可以假设它们都有一个 __init__ 方法,该方法将 _fields 设置为某个列表。但是,如果您只需要 Model 类可重用(而不是元类),您可以直接将逻辑放入 Model.__init__ (这似乎是您目前所做的)。元类用于深层魔法。我从没想过它们会很容易理解(如果很容易,你就不需要元类了)。 是的,只是因为我希望我的Model 类可重用,我需要使它简单。我的意思是,将任何相关数据存储在一个单独的类中,以便使用Model 类的人看不到字段计算和其他内容。另外,我希望只需要 public 实例和类变量,因此没有变量可以充当某种功能的助手。这就是为什么我试图将大部分“后台”逻辑移动到其他实体,例如元类或其他类。但是,我发现很难将此类与实例逻辑分开...... 也许你应该把“幕后”的东西放在一个基类中,Model 继承自。这不会让这些东西不出现在类或实例上,但它会将作为实现细节的位从公共接口中分离出来。 如果一个类没有实现__init__,但它的基类实现了,这个包装__init__的解决方案就会出现问题。在这种情况下,元类会为确实调用基类__init__方法的类创建一个全新的__init__方法,从而使类不适当地(未)初始化。【参考方案3】:

如果需要为每个实例设置,则放置代码以计算它的位置在__init__ 中。

但是,请注意,类变量可通过实例使用,除非被实例变量遮蔽。

【讨论】:

这是我想过的,但正如我所说,它最终会在我的类'__init__()方法中添加太多代码。这就是为什么我一直在寻找一种在我的班级周围定义额外事物的方法,使用元类或其他任何合适的方法。这是唯一的方法吗? @AndreiHorak -- 你可以让__init__ 在你的类中调用你想要的任何其他方法。 @AndreiHorak 你可以使用__new__,但没有理由这样做。正如@mgilson 所说,如果您想将代码保留在您的 init 之外,请将其放在普通方法中。【参考方案4】:

您可以使用工厂而不是元类来完成这一切。

工厂可以创建类型,然后也可以创建这些类型的实例 - 但是在初始化时而不是在类创建时这样做。将相关知识保存在一个地方(工厂),但不会试图将两种不同的工作硬塞到一个操作中。

OTOH,您还可以使用您在类创建时通过闭包获得的信息,在每个实例的初始化时动态组合一个函数

class ExMeta(type):

        def __new__(cls, name, parents, kwargs):
            kwargs['ADDED_DYNAMICALLY'] = True

            secret_knowledge = 42
            def fake_init(*args, **kwargs):
                print "hello world... my args are:",  args, kwargs
                print "the meaning of the universe is ", secret_knowledge
            kwargs['_post_init'] = fake_init
            return super(ExMeta, cls).__new__(cls, name, parents, kwargs)

class Example(object):
    SOME_ATTR = 1
    SOME_OTHER = 2
    __metaclass__ = ExMeta       

    def __init__(self):
        self._post_init()


bob = Example()
> hello world... my args are: (<__main__.Example object at 0x0000000002425BA8>,) 
> the meaning of the universe is  42

【讨论】:

这是上面建议的,但它不能保持Model 类的干净。它添加了 _post_init() 类参数,它使模型类变得混乱,这应该很简单;此外,使用 Model 类的人期望使用简单的 API,但由于在元类中添加了 _post_init(),因此无法实现这一点,这对于简单的 API 来说过于复杂。 初始化包装器解决方案可以让您对用户隐藏它。在没有一行代码的情况下,没有其他方法可以摆脱运行时初始化的困境,无论是像我这样的回调还是超级调用。我更喜欢明确说明,以便用户可以决定何时调用它,包装器版本可能最接近解决方案【参考方案5】:

更新可能会显示我的想法:

class MetaKlass(object):
    def __new__(cls, *args, **kwargs):
        class_methods = 'foo'
        _ins = super(MetaKlass, cls).__new__(cls)
        for k, v in kwargs.iteritems():
            if k in class_methods:
                _do = _ins.__getattribute__(k)
                setattr(_ins, k, _do(v)) # self monkey patching:
                                        # classmethod is replaced by its output
            else:
                setattr(_ins, k, v)
        return _ins

    def __init__(self, arg1, arg2, *args, **kwargs):
        self.a = arg1
        self.b = arg2

    @classmethod
    def foo(cls, x):
        return 42

所以我们得到:

>>> from overflow_responses import MetaKlass
>>> klass_dict = 'foo': 1, 'angel': 2
>>> k = MetaKlass(3, 7, **klass_dict)
>>> k.foo, k.angel, k.a, k.b
(42, 2, 3, 7)

您是否正在寻找这样的东西:

>>> kwd = 'a': 2  # the 'class variables dict'
>>> class MetaKlass(object):
...     def __new__(cls, kwd):
...             _ins = super(MetaKlass, cls).__new__(cls)
...             for k, v in kwd.iteritems():
...                     setattr(_ins, k, v)
...             return _ins
...
>>> mk = MetaKlass(kwd)
>>> mk.a
2
>>>

代码应该相当简单。当我将一组方法组织到一个类中时,其中许多方法都依赖于一组类值和特定的实例值。

【讨论】:

这不是我所要求的。设置类变量的元类部分已经完成,我在问如何在类创建时设置实例变量——或者为它们提供默认值。 @AndreiHorak 这实际上就是这样做的。它的主要问题是它可能不会调用__init__(除非超级新人这样做)。 @AndreiHorak 我提供了一个更具体的例子来说明我的想法。这是否解决了您关于致电__init__ 的评论? 好吧,元类继承自type,而不是object,在我的例子中,我使用元类专门在创建时解析类变量。您的示例只是定义了一个基类,它为实例变量提供值,但在类 instantiation 时间,而不是在类 creation 时间。我正在寻找一种在类 creation 时间提供这种功能的方法,因为在 instantiation 时间提供它会导致在我的情况下重复代码,并且它将是更难理解。 投反对票。正如@AndreiHorak 所说,元类从类型继承。不用说,您没有将它用作元类,也没有提供替代方案的解释。

以上是关于从元类设置实例变量的主要内容,如果未能解决你的问题,请参考以下文章

类和元类

C++:使用友元类限制对象实例化

从元类访问构造函数的参数

如何记录从元类继承的方法?

class_getClassVariable() 有啥作用?

题型:python