如何使“仅关键字"数据类的字段? [英] How to make "keyword-only" fields with dataclasses?

查看:57
本文介绍了如何使“仅关键字"数据类的字段?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

从3.0起支持仅使自变量关键字成为可能:

Since 3.0 there is support to make an argument keyword only:

class S3Obj:
    def __init__(self, bucket, key, *, storage_class='Standard'):
        self.bucket = bucket
        self.key = key
        self.storage_class = storage_class

如何使用数据类获得这种签名?这样的东西,但最好不要SyntaxError:

How to get that kind of signature using dataclasses? Something like this, but preferably without the SyntaxError:

@dataclass
class S3Obj:
    bucket: str
    key: str
    *
    storage_class: str = 'Standard'

理想上是声明性的,但是使用__post_init__钩子和/或替换类装饰器也可以-只要代码可重用即可.

Ideally declarative, but using the __post_init__ hook and/or a replacement class decorator is fine too - as long as the code is reusable.

也许像这样的语法,使用省略号文字

maybe something like this syntax, using an ellipsis literal

@mydataclass
class S3Obj:
    bucket: str
    key: str
    ...
    storage_class: str = 'Standard'

推荐答案

执行此操作不会从dataclasses那里获得太多帮助.没有办法说应该使用仅关键字参数初始化字段,并且__post_init__钩子不知道原始构造函数参数是否通过关键字传递.此外,也没有很好的方法来对InitVar进行自省,更不用说将InitVar标记为仅关键字了.

You're not going to get much help from dataclasses when doing this. There's no way to say that a field should be initialized by keyword-only argument, and the __post_init__ hook doesn't know whether the original constructor arguments were passed by keyword. Also, there's no good way to introspect InitVars, let alone mark InitVars as keyword-only.

至少,您必须替换生成的__init__.可能最简单的方法是手动定义__init__.如果您不想这样做,则最可靠的方法可能是创建字段对象,并在metadata中将其标记为kwonly,然后在您自己的装饰器中检查元数据.这比听起来还要复杂:

At minimum, you'll have to replace the generated __init__. Probably the simplest way is to just define __init__ by hand. If you don't want to do that, probably the most robust way is to create field objects and mark them kwonly in the metadata, then inspect the metadata in your own decorator. This is even more complicated than it sounds:

import dataclasses
import functools
import inspect

# Helper to make calling field() less verbose
def kwonly(default=dataclasses.MISSING, **kwargs):
    kwargs.setdefault('metadata', {})
    kwargs['metadata']['kwonly'] = True
    return dataclasses.field(default=default, **kwargs)

def mydataclass(_cls, *, init=True, **kwargs):
    if _cls is None:
        return functools.partial(mydataclass, **kwargs)

    no_generated_init = (not init or '__init__' in _cls.__dict__)
    _cls = dataclasses.dataclass(_cls, **kwargs)
    if no_generated_init:
        # No generated __init__. The user will have to provide __init__,
        # and they probably already have. We assume their __init__ does
        # what they want.
        return _cls

    fields = dataclasses.fields(_cls)
    if any(field.metadata.get('kwonly') and not field.init for field in fields):
        raise TypeError('Non-init field marked kwonly')

    # From this point on, ignore non-init fields - but we don't know
    # about InitVars yet.
    init_fields = [field for field in fields if field.init]
    for i, field in enumerate(init_fields):
        if field.metadata.get('kwonly'):
            first_kwonly = field.name
            num_kwonly = len(init_fields) - i
            break
    else:
        # No kwonly fields. Why were we called? Assume there was a reason.
        return _cls

    if not all(field.metadata.get('kwonly') for field in init_fields[-num_kwonly:]):
        raise TypeError('non-kwonly init fields following kwonly fields')

    required_kwonly = [field.name for field in init_fields[-num_kwonly:]
                       if field.default is field.default_factory is dataclasses.MISSING]

    original_init = _cls.__init__

    # Time to handle InitVars. This is going to get ugly.
    # InitVars don't show up in fields(). They show up in __annotations__,
    # but the current dataclasses implementation doesn't understand string
    # annotations, and we want an implementation that's robust against
    # changes in string annotation handling.
    # We could inspect __post_init__, except there doesn't have to be a
    # __post_init__. (It'd be weird to use InitVars with no __post_init__,
    # but it's allowed.)
    # As far as I can tell, that leaves inspecting __init__ parameters as
    # the only option.

    init_params = tuple(inspect.signature(original_init).parameters)
    if init_params[-num_kwonly] != first_kwonly:
        # InitVars following kwonly fields. We could adopt a convention like
        # "InitVars after kwonly are kwonly" - in fact, we could have adopted
        # "all fields after kwonly are kwonly" too - but it seems too likely
        # to cause confusion with inheritance.
        raise TypeError('InitVars after kwonly fields.')
    # -1 to exclude self from this count.
    max_positional = len(init_params) - num_kwonly - 1

    @functools.wraps(original_init)
    def __init__(self, *args, **kwargs):
        if len(args) > max_positional:
            raise TypeError('Too many positional arguments')
        check_required_kwargs(kwargs, required_kwonly)
        return original_init(self, *args, **kwargs)
    _cls.__init__ = __init__

    return _cls

def check_required_kwargs(kwargs, required):
    # Not strictly necessary, but if we don't do this, error messages for
    # required kwonly args will list them as positional instead of
    # keyword-only.
    missing = [name for name in required if name not in kwargs]
    if not missing:
        return
    # We don't bother to exactly match the built-in logic's exception
    raise TypeError(f"__init__ missing required keyword-only argument(s): {missing}")

用法示例:

@mydataclass
class S3Obj:
    bucket: str
    key: str
    storage_class: str = kwonly('Standard')

这已经过测试,但是没有我想要的彻底.

This is somewhat tested, but not as thoroughly as I would like.

您无法获得您使用...提出的语法,因为...不会执行元类或装饰器可以看到的任何操作.您可以通过实际上触发名称查找或赋值的内容(例如kwonly_start = True)获得非常接近的内容,以便元类可以看到它的发生.但是,由于许多事情需要专门处理,因此编写此函数的可靠实现很复杂.如果不仔细处理,继承,typing.ClassVardataclasses.InitVar,批注中的前向引用等都将导致问题.继承可能导致最多的问题.

You can't get the syntax you propose with ..., because ... doesn't do anything a metaclass or decorator can see. You can get something pretty close with something that actually triggers name lookup or assignment, like kwonly_start = True, so a metaclass can see it happen. However, a robust implementation of this is complicated to write, because there are a lot of things that need dedicated handling. Inheritance, typing.ClassVar, dataclasses.InitVar, forward references in annotations, etc. will all cause problems if not handled carefully. Inheritance probably causes the most problems.

不能处理所有杂乱无章的概念验证可能看起来像这样:

A proof-of-concept that doesn't handle all the fiddly bits might look like this:

# Does not handle inheritance, InitVar, ClassVar, or anything else
# I'm forgetting.

class POCMetaDict(dict):
    def __setitem__(self, key, item):
        # __setitem__ instead of __getitem__ because __getitem__ is
        # easier to trigger by accident.
        if key == 'kwonly_start':
            self['__non_kwonly'] = len(self['__annotations__'])
        super().__setitem__(key, item)

class POCMeta(type):
    @classmethod
    def __prepare__(cls, name, bases, **kwargs):
        return POCMetaDict()
    def __new__(cls, name, bases, classdict, **kwargs):
        classdict.pop('kwonly_start')
        non_kwonly = classdict.pop('__non_kwonly')

        newcls = super().__new__(cls, name, bases, classdict, **kwargs)
        newcls = dataclass(newcls)

        if non_kwonly is None:
            return newcls

        original_init = newcls.__init__

        @functools.wraps(original_init)
        def __init__(self, *args, **kwargs):
            if len(args) > non_kwonly:
                raise TypeError('Too many positional arguments')
            return original_init(self, *args, **kwargs)

        newcls.__init__ = __init__
        return newcls

您会喜欢使用它

class S3Obj(metaclass=POCMeta):
    bucket: str
    key: str

    kwonly_start = True

    storage_class: str = 'Standard'

这未经测试.

这篇关于如何使“仅关键字"数据类的字段?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆