简体   繁体   English

使用元类进行默认属性初始化

[英]using metaclass for default attribute init

Let's say I have a resource class that looks as such:假设我有一个资源 class 看起来像这样:

class Resource(metaclass=ResourceInit):
    def __init__(self, a, b):
        self.a = a
        self.b = b

My goal is to create a metaclass, ie ResourceInit , that handles automatically assigning attributes to the instantiated Resource .我的目标是创建一个元类,即ResourceInit ,它处理自动将属性分配给实例化的Resource

The following code I have, that does not work:我的以下代码不起作用:

config = {"resources": {"some_resource": {"a": "testA", "b": "testB"}}}

class ResourceInit(type):

    def __call__(self, *args, **kwargs):

        obj = super(ResourceInit, self)

        argspec = inspect.getargspec(obj.__self__.__init__)

        defaults = {}

        for argument in [x for x in argspec.args if x != "self"]:
            for settings in config["resources"].values():
                for key, val in settings.items():
                    if key == argument:
                        defaults.update({argument: val})
            if os.environ.get(argument):
                defaults.update({argument: os.environ[argument]})

        defaults.update(kwargs)
        for key, val in defaults.items():
            setattr(obj, key, val)
        return obj

The idea is, using this metaclass, upon instantiating这个想法是,使用这个元类,在实例化

res = Resource()

will automatically populate a and b if it exists in the config or as an environment variable..如果ab存在于config中或作为环境变量,将自动填充它。

Obviously this is a dummied example, where a and b will be substantially more specific, ie xx__resource__name .显然,这是一个虚拟示例,其中ab将更加具体,即xx__resource__name

My questions:我的问题:

  1. Can you pass an argument to the metaclass within the subclass, ie resource="some_resource"您可以将参数传递给子类中的元类,即resource="some_resource"
  2. How can I get this to work as any normal class if either config or os.environ is not set, ie x = Test() results in TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'如果未设置configos.environ ,即x = Test()导致TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'我怎样才能让它像任何正常的 class 一样工作

Alternative选择

You are making this more complicated than it needs to be.你让这变得比它需要的更复杂。 The simple solution looks like this:简单的解决方案如下所示:

def get_configured(name, value, config):
    if value is None:
        try:
            value = next(c for c in config['resources'].values() if name in c)[name]
        except StopIteration:
            value = os.environ.get(name, None)
    return value

class Resource:
    def __init__(self, a=None, b=None):
        self.a = get_configured('a', a, config)
        self.b = get_configured('a', b, config)

The function is reusable, and you can easily modify it to minimize the amount of boilerplate each class will have. function 是可重复使用的,您可以轻松修改它以最大限度地减少每个 class 将具有的样板数量。

Full Answer完整答案

However, if you do insist on taking the metaclass road, you can simplify that as well.但是,如果您确实坚持走元类之路,您也可以将其简化。 You can add as many keyword-only arguments as you want to your class definition ( Question 1 ).您可以在 class 定义中添加任意数量的仅关键字 arguments(问题 1 )。

class Resource(metaclass=ResourceInit, config=config): ...

config , and any other arguments besides metaclass will get passed directly to the __call__ method of the meta-metaclass. config和除metaclass之外的任何其他 arguments 将直接传递给元元类的__call__方法。 From there, they get passed to __new__ and __init__ in the metaclass.从那里,它们被传递给元类中的__new____init__ You must therefore implement __new__ .因此,您必须实施__new__ You might be tempted to implement __init__ instead, but object.__init_subclass__ , which is called from type.__new__ , raises an error if you pass in keyword arguments:您可能很想实现__init__ ,但是如果您传入关键字 arguments ,从type.__new__调用的object.__init_subclass__ .__init_subclass__ 会引发错误:

class ResourceInit(type):
    def __new__(meta, name, bases, namespace, *, config, **kwargs):
        cls = super().__new__(meta, name, bases, namespace, **kwargs)

Notice that last arguments, config and kwargs .注意最后一个 arguments, configkwargs Positional arguments are passed as bases .位置 arguments 作为bases传递。 kwargs must not contain unexpected arguments before it is passed to type.__new__ , but should pass through anything that __init_subclass__ on your class expects. kwargs在传递给type.__new__之前不得包含意外的 arguments ,但应通过__init_subclass__上的 __init_subclass__ 期望的任何内容。

There is no need to use __self__ when you have direct access to namespace .当您可以直接访问namespace时,无需使用__self__ Keep in mind that this will only update the default if your __init__ method is actually defined.请记住,如果您的__init__方法被实际定义,这只会更新默认值。 You likely don't want to mess with the parent __init__ .你可能不想惹父母__init__ To be safe, let's raise an error if __init__ is not present:为了安全起见,如果__init__不存在,让我们提出一个错误:

        if '__init__' not in namespace or not callable(getattr(cls, '__init__')):
            raise ValueError(f'Class {name} must specify its own __init__ function')
        init = getattr(cls, '__init__')

Now we can build up the default values using a function similar to what I showed above.现在我们可以使用类似于我上面显示的 function 来建立默认值。 You have to be careful to avoid setting defaults in the wrong order.您必须小心避免以错误的顺序设置默认值。 So while all keyword-only arguments can have optional defaults, only the positional arguments at the end of the list get them.因此,虽然所有仅关键字 arguments 都可以具有可选的默认值,但只有列表末尾的位置 arguments 可以获得它们。 That means that the loop over positional defaults should start from the end, and should stop immediately as soon as a name with no defaults is found:这意味着位置默认值的循环应该从末尾开始,并且一旦找到没有默认值的名称就应该立即停止:

def lookup(name, configuration):
    try:
        return next(c for c in configuration['resources'].values() if name in c)[name]
    except StopIteration:
        return os.environ.get(name)

...

        spec = inspect.getfullargspec(init)

        defaults = []
        for name in spec.args[:0:-1]:
            value = lookup(name, config)
            if value is None:
                break
            defaults.append(value)

        kwdefaults = {}
        for name in spec.kwonlyargs:
            value = lookup(name, config)
            if value is not None:
                kwdefaults[name] = value

The expression spec.args[:0:-1] iterates backwards through all the positional arguments except the first one.表达式spec.args[:0:-1]向后迭代除第一个之外的所有位置 arguments。 Remember that self is a conventional, not mandatory name.请记住, self是一个传统的而非强制性的名称。 Removing it by index is therefore much more robust than removing it by name.因此,按索引删除它比按名称删除它更健壮。

The key to making the defaults and kwdefaults values mean anything is to assign them to the __defaults__ and __kwdefaults__ on the actual __init__ function object ( Question 2 ): defaultskwdefaults值的关键是将它们分配给实际__init__ function __kwdefaults__上的__defaults__和 __kwdefaults__ (问题 2 ):

        init.__defaults__ = tuple(defaults[::-1])
        init.__kwdefaults__ = kwdefaults
        return cls

__defaults__ must be reversed and converted to a tuple. __defaults__必须反转并转换为元组。 The former is necessary to get the order of the arguments right.前者是正确获取 arguments 的顺序所必需的。 The latter is required by the __defaults__ descriptor. __defaults__描述符需要后者。

Quick Test快速测试

>>> configR = {"resources": {"some_resource": {"a": "testA", "b": "testB"}}}
>>> class Resource(metaclass=ResourceInit, config=configR):
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
... 
>>> r = Resource()
>>> r.a
'testA'
>>> r.b
'testB'

TL;DR TL;博士

def lookup(name, configuration):
    try:
        return next(c for c in configuration['resources'].values() if name in c)[name]
    except StopIteration:
        return os.environ.get(name)

class ResourceInit(type):
    def __new__(meta, name, bases, namespace, **kwargs):
        config = kwargs.pop('config')
        cls = super().__new__(meta, name, bases, namespace, **kwargs)

        if '__init__' not in namespace or not callable(getattr(cls, '__init__')):
            raise ValueError(f'Class {name} must specify its own __init__ function')
        init = getattr(cls, '__init__')
        spec = inspect.getfullargspec(init)

        defaults = []
        for name in spec.args[:0:-1]:
            value = lookup(name, config)
            if value is None:
                break
            defaults.append(value)

        kwdefaults = {}
        for name in spec.kwonlyargs:
            value = lookup(name, config)
            if value is not None:
                kwdefaults[name] = value

        init.__defaults__ = tuple(defaults[::-1])
        init.__kwdefaults__ = kwdefaults

        return cls

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM