从元类设置实例变量

4
给定一个元类,或者更简单地说,type(),最后一个参数是类字典,负责类变量。我想知道,有没有一种方法可以从元类中设置实例变量?
我使用元类的原因是我需要收集在类创建时定义的一些变量并对它们进行处理。但是,这个处理的结果需要附加到类的每个实例上,而不是类本身,因为结果将是一个列表,它将因一个类实例而异。
我会提供一个示例,以便更容易理解我的意思。
我有一个名为Model的类,定义如下:
class Model(object):
    __metaclass__ = ModelType

    has_many = None

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

has_many 变量将在类定义时由任何需要的模型填充。在__init__()中,我只是将提供的关键字分配给实例变量_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变量,然后将该变量的内容添加到受限字段列表中。
现在来到重要的部分。在实例化模型时,我需要_fields由实例化模型使用的关键字参数和元类中处理的受限字段组成。如果_fieldsModel类中的变量,那么这将非常容易,但是由于它是一个实例变量,我不知道如何将受限字段添加到其中。
综上所述,有没有办法实现这一点?或者有更好的处理方式吗?(我考虑仅使用元类并在模型上设置_restricted_fields类变量,然后在类__init__() 中使用此变量将受限字段添加到普通字段中,但很快模型类将充斥着代码,我认为应该将其放在代码的另一部分)。

这是你要找的吗:当你创建一个类的新实例时,一些与现有实例相关联的变量会发生改变? - Cole
实际上,这正是我想要避免的。这就是为什么我正在寻找在类创建时定义的机制,它将为在实例化类时创建的变量分配默认值。 - linkyndy
5个回答

10
使用元类并不是正确的方法。 元类修改的是类创建行为而不是实例创建行为。 您应该使用__init____new__函数来修改实例创建行为。希望使用元类来处理这些事情就像是用锤子代替螺丝刀把螺丝钉在墙上一样。 ;-)
我建议您使用__new__来实现所需的功能。来自Python文档的描述:

__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类。它向由它创建的任何Model类添加了一个_restricted_fields类变量(该值是包含has_many类变量的列表)。

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

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


这正是我所想的,但我应该如何将元类中处理过的变量链接到类的__init__()__new__()中呢?我能想到的唯一方法是在Model类上创建一些类变量,并在实例化时使用它们,但正如我所说,这会使类看起来杂乱无章且复杂。 - linkyndy
听起来挺公平的。但问题是,这些实例变量恰好是元类处理的变量。也就是说,例如你提供的 _restricted_fields。这些是我需要在每个实例中拥有的变量。而且根据您的示例,元类完成其工作后,这些变量已在Model类上定义。但我担心的是,我不喜欢这些变量存储在Model类上,因为它们是“后台”内容,不应该对使用Model类的人可见。 - linkyndy
@AndreiHorak 看起来我的回答正是你所需要的(我应该去做销售工作)。欢迎任何评论/反馈。;-) - siebz0r
@AndreiHorak 我已经添加了一份说明,希望这有助于澄清事情。;-) - siebz0r
我认为你没有理解我的关注点。我知道你的代码是做什么的,我只是在寻找另一种传递这些受限字段到实例的方式,因为正如我所说,它会使“Model”类变得混乱,而这个类应该更简单,因为它公开了一个API。 - linkyndy
显示剩余9条评论

3

设置实例变量的地方在类的__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()”的基础上添加了一些额外的东西,我相信这并不是那么糟糕,对吧? - linkyndy
如果您可以依赖使用它的常规类表现适当(而不是试图防范万一),那么您可以稍微简化元类。也就是说,您可以假定它们都有一个__init__方法,该方法将_fields设置为某个列表。但是,如果您只需要Model类可重用(而不是元类),则可以直接在Model.__init__中放置逻辑(这似乎是您迄今为止所做的)。元类是深层魔法。我从来不指望它们易于理解(如果容易,您就不需要元类了)。 - Blckknght
是的,因为我希望我的“Model”类可重用,所以我需要让它变得_简单_。这意味着将任何相关数据存储在单独的类中,以便字段计算和其他内容对使用“Model”类的人不可见。此外,我只想要必需的公共实例和类变量,因此没有变量作为某种功能的辅助程序。这就是为什么我试图将大部分“后台”逻辑移动到其他实体(例如元类或其他类)的原因。然而,我发现很难区分这个类与实例逻辑... - linkyndy
2
也许你应该将“幕后”内容放在一个基类中,然后让Model继承它。这样做不会使得这些内容消失在类或实例上,但可以将实现细节与公共接口分离开来。 - Blckknght
这种包装__init__的解决方案存在一个问题,如果一个类没有实现__init__但其基类有,则元类会为该类创建一个全新的__init__方法,而该方法不会调用基类的__init__方法,从而使得该类未被适当地初始化。 - pallgeuer

1
你可以使用工厂而不是元类来完成所有这些操作。
工厂可以创建类型,然后还可以创建这些类型的实例 - 但是在初始化时而不是在类创建时执行。这将相关的知识放在一个地方(即工厂),但不会试图把两种不同的工作塞进一个操作中。
另一方面,你也可以动态组合一个函数,在每个实例的初始化时使用在类创建时拥有的信息通过闭包来运行。
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增加了复杂性。 - linkyndy
初始化包装器解决方案可以让您将此隐藏在用户之外。没有其他方法可以保证运行时初始化而不需要一行代码,无论是像我的回调还是超级调用。我更喜欢明确说明,以便用户可以决定何时调用它,包装器版本可能是最接近实现的解决方案。 - theodox

1

如果需要每个实例进行设置,计算任何内容的代码应放在__init__中。

但是,请注意,类变量可以通过实例访问,除非被实例变量隐藏。


这是我想到的,但正如我所说,它最终会在我的类的__init __()方法中添加太多代码。这就是为什么我正在寻找一种定义类周围额外内容的方法,使用元类或其他适合的方法。这是唯一的解决方法吗? - linkyndy
1
@AndreiHorak -- 你可以让__init__调用任何你想要的类中的其他方法。 - mgilson
1
@AndreiHorak 你可以使用__new__,但没有理由这样做。就像@mgilson说的那样,如果你想将代码从init中分离出来,可以将其放在一个普通的方法中。 - Marcin

0

更新,可能展示了我所思考的内容:

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
>>>

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


这并不完全是我所要求的。关于设置类变量的元类部分已经完成,我想知道如何在创建类时设置实例变量或为其提供默认值。 - linkyndy
1
@AndreiHorak 这实际上确实做到了这一点。它的主要问题在于它可能不会调用__init__(除非超级新这样做)。 - Marcin
@AndreiHorak 我提供了一个更具体的例子来解释我在想什么。这能否解决您关于调用 __init__ 的评论? - Cole
元类继承自type,而不是object,我在这种情况下使用元类特别是为了在创建时解析类变量。你的例子只是定义了一个基类,它提供了实例变量的值,但是在类实例化时,而不是在类创建时。我正在寻找一种在类创建时提供此类功能的方法,因为在实例化时提供它将导致我的代码重复,并且更难理解。 - linkyndy
被踩了。元类从type继承,就像@AndreiHorak所说的那样。不用说,你没有将其用作元类,并且也没有提供替代方案的解释。 - siebz0r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接