[Python-ideas] Dataclasses, keyword args, and inheritance

Guido van Rossum guido at python.org
Mon Jan 29 14:05:03 EST 2018


I think that settles it -- there's no reason to try to implement this.

On Mon, Jan 29, 2018 at 10:51 AM, George Leslie-Waksman <waksman at gmail.com>
wrote:

> attrs' seems to also not allow mandatory attributes to follow optional one:
>
> In [14]: @attr.s
>     ...: class Baz:
>     ...:     a = attr.ib(default=attr.Factory(list))
>     ...:     b = attr.ib()
>     ...:
> ------------------------------------------------------------
> ---------------
> ValueError                                Traceback (most recent call last)
> <ipython-input-14-2c63f3f229a5> in <module>()
> ----> 1 @attr.s
>       2 class Baz:
>       3     a = attr.ib(default=attr.Factory(list))
>       4     b = attr.ib()
>       5
>
> /Users/waksman/.pyenv/versions/3.6.1/envs/temp/lib/
> python3.6/site-packages/attr/_make.py in attrs(maybe_cls, these, repr_ns,
> repr, cmp, hash, init, slots, frozen, str, auto_attribs)
>     700         return wrap
>     701     else:
> --> 702         return wrap(maybe_cls)
>     703
>     704
>
> /Users/waksman/.pyenv/versions/3.6.1/envs/temp/lib/
> python3.6/site-packages/attr/_make.py in wrap(cls)
>     669             raise TypeError("attrs only works with new-style
> classes.")
>     670
> --> 671         builder = _ClassBuilder(cls, these, slots, frozen,
> auto_attribs)
>     672
>     673         if repr is True:
>
> /Users/waksman/.pyenv/versions/3.6.1/envs/temp/lib/
> python3.6/site-packages/attr/_make.py in __init__(self, cls, these,
> slots, frozen, auto_attribs)
>     369
>     370     def __init__(self, cls, these, slots, frozen, auto_attribs):
> --> 371         attrs, super_attrs = _transform_attrs(cls, these,
> auto_attribs)
>     372
>     373         self._cls = cls
>
> /Users/waksman/.pyenv/versions/3.6.1/envs/temp/lib/
> python3.6/site-packages/attr/_make.py in _transform_attrs(cls, these,
> auto_attribs)
>     335                 "No mandatory attributes allowed after an
> attribute with a "
>     336                 "default value or factory.  Attribute in question:
> {a!r}"
> --> 337                 .format(a=a)
>     338             )
>     339         elif had_default is False and \
>
> ValueError: No mandatory attributes allowed after an attribute with a
> default value or factory.  Attribute in question: Attribute(name='b',
> default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True,
> metadata=mappingproxy({}), type=None, converter=None)
>
>
> On Fri, Jan 26, 2018 at 1:44 PM Guido van Rossum <guido at python.org> wrote:
>
>> What does attrs' solution for this problem look like?
>>
>> On Fri, Jan 26, 2018 at 11:11 AM, George Leslie-Waksman <
>> waksman at gmail.com> wrote:
>>
>>> Even if we could inherit the setting, I would think that we would still
>>> want to require the code be explicit. It seems worse to implicitly require
>>> keyword only arguments for a class without giving any indication in the
>>> code.
>>>
>>> As it stands, the current implementation does not allow a later subclass
>>> to be declared without `keyword_only=True` so we could handle this case by
>>> adding a note to the `TypeError` message about considering the keyword_only
>>> flag.
>>>
>>> How do I got about putting together a proposal to get this into 3.8?
>>>
>>> --George
>>>
>>>
>>> On Thu, Jan 25, 2018 at 5:12 AM Eric V. Smith <eric at trueblade.com>
>>> wrote:
>>>
>>>> I'm not completely opposed to this feature. But there are some cases to
>>>> consider. Here's the first one that occurs to me: note that due to the
>>>> way dataclasses work, it would need to be used everywhere down an
>>>> inheritance hierarchy. That is, if an intermediate base class required
>>>> it, all class derived from that intermediate base would need to specify
>>>> it, too. That's because each class just makes decisions based on its
>>>> fields and its base classes' fields, and not on any flags attached to
>>>> the base class. As it's currently implemented, a class doesn't remember
>>>> any of the decorator's arguments, so there's no way to look for this
>>>> information, anyway.
>>>>
>>>> I think there are enough issues here that it's not going to make it in
>>>> to 3.7. It would require getting a firm proposal together, selling the
>>>> idea on python-dev, and completing the implementation before Monday. But
>>>> if you want to try, I'd participate in the discussion.
>>>>
>>>> Taking Ivan's suggestion one step further, a way to do this currently is
>>>> to pass init=False and then write another decorator that adds the
>>>> kw-only __init__. So the usage would be:
>>>>
>>>> @dataclass
>>>>      class Foo:
>>>>          some_default: dict = field(default_factory=dict)
>>>>
>>>> @kw_only_init
>>>> @dataclass(init=False)
>>>> class Bar(Foo):
>>>>      other_field: int
>>>>
>>>> kw_only_init(cls) would look at fields(cls) and construct the __init__.
>>>> It would be a hassle to re-implement dataclasses's _init_fn function,
>>>> but it could be made to work (in reality, of course, you'd just copy it
>>>> and hack it up to do what you want). You'd also need to use some private
>>>> knowledge of InitVars if you wanted to support them (the stock
>>>> fields(cls) doesn't return them).
>>>>
>>>> For 3.8 we can consider changing dataclasses's APIs if we want to add
>>>> this.
>>>>
>>>> Eric.
>>>>
>>>> On 1/25/2018 1:38 AM, George Leslie-Waksman wrote:
>>>> > It may be possible but it makes for pretty leaky abstractions and it's
>>>> > unclear what that custom __init__ should look like. How am I supposed
>>>> to
>>>> > know what the replacement for default_factory is?
>>>> >
>>>> > Moreover, suppose I want one base class with an optional argument and
>>>> a
>>>> > half dozen subclasses each with their own required argument. At that
>>>> > point, I have to write the same __init__ function a half dozen times.
>>>> >
>>>> > It feels rather burdensome for the user when an additional flag (say
>>>> > "kw_only=True") and a modification to:
>>>> > https://github.com/python/cpython/blob/master/Lib/dataclasses.py#L294
>>>>  that
>>>> > inserted `['*']` after `[self_name]` if the flag is specified could
>>>> > ameliorate this entire issue.
>>>> >
>>>> > On Wed, Jan 24, 2018 at 3:22 PM Ivan Levkivskyi <levkivskyi at gmail.com
>>>> > <mailto:levkivskyi at gmail.com>> wrote:
>>>> >
>>>> >     It is possible to pass init=False to the decorator on the subclass
>>>> >     (and supply your own custom __init__, if necessary):
>>>> >
>>>> >     @dataclass
>>>> >     class Foo:
>>>> >          some_default: dict = field(default_factory=dict)
>>>> >
>>>> >     @dataclass(init=False) # This works
>>>> >     class Bar(Foo):
>>>> >          other_field: int
>>>> >
>>>> >     --
>>>> >     Ivan
>>>> >
>>>> >
>>>> >
>>>> >     On 23 January 2018 at 03:33, George Leslie-Waksman
>>>> >     <waksman at gmail.com <mailto:waksman at gmail.com>> wrote:
>>>> >
>>>> >         The proposed implementation of dataclasses prevents defining
>>>> >         fields with defaults before fields without defaults. This can
>>>> >         create limitations on logical grouping of fields and on
>>>> inheritance.
>>>> >
>>>> >         Take, for example, the case:
>>>> >
>>>> >         @dataclass
>>>> >         class Foo:
>>>> >              some_default: dict = field(default_factory=dict)
>>>> >
>>>> >         @dataclass
>>>> >         class Bar(Foo):
>>>> >              other_field: int
>>>> >
>>>> >         this results in the error:
>>>> >
>>>> >                5 @dataclass
>>>> >         ----> 6 class Bar(Foo):
>>>> >                7     other_field: int
>>>> >                8
>>>> >
>>>> >         ~/.pyenv/versions/3.6.2/envs/clover_pipeline/lib/python3.6/
>>>> site-packages/dataclasses.py
>>>> >         in dataclass(_cls, init, repr, eq, order, hash, frozen)
>>>> >              751
>>>> >              752     # We're called as @dataclass, with a class.
>>>> >         --> 753     return wrap(_cls)
>>>> >              754
>>>> >              755
>>>> >
>>>> >         ~/.pyenv/versions/3.6.2/envs/clover_pipeline/lib/python3.6/
>>>> site-packages/dataclasses.py
>>>> >         in wrap(cls)
>>>> >              743
>>>> >              744     def wrap(cls):
>>>> >         --> 745         return _process_class(cls, repr, eq, order,
>>>> >         hash, init, frozen)
>>>> >              746
>>>> >              747     # See if we're being called as @dataclass or
>>>> >         @dataclass().
>>>> >
>>>> >         ~/.pyenv/versions/3.6.2/envs/clover_pipeline/lib/python3.6/
>>>> site-packages/dataclasses.py
>>>> >         in _process_class(cls, repr, eq, order, hash, init, frozen)
>>>> >              675                                 #  in __init__.  Use
>>>> >         "self" if possible.
>>>> >              676                                 '__dataclass_self__'
>>>> if
>>>> >         'self' in fields
>>>> >         --> 677                                     else 'self',
>>>> >              678                                 ))
>>>> >              679     if repr:
>>>> >
>>>> >         ~/.pyenv/versions/3.6.2/envs/clover_pipeline/lib/python3.6/
>>>> site-packages/dataclasses.py
>>>> >         in _init_fn(fields, frozen, has_post_init, self_name)
>>>> >              422                 seen_default = True
>>>> >              423             elif seen_default:
>>>> >         --> 424                 raise TypeError(f'non-default argument
>>>> >         {f.name <http://f.name>!r} '
>>>> >              425                                 'follows default
>>>> argument')
>>>> >              426
>>>> >
>>>> >         TypeError: non-default argument 'other_field' follows default
>>>> >         argument
>>>> >
>>>> >         I understand that this is a limitation of positional arguments
>>>> >         because the effective __init__ signature is:
>>>> >
>>>> >         def __init__(self, some_default: dict = <something>,
>>>> >         other_field: int):
>>>> >
>>>> >         However, keyword only arguments allow an entirely reasonable
>>>> >         solution to this problem:
>>>> >
>>>> >         def __init__(self, *, some_default: dict = <something>,
>>>> >         other_field: int):
>>>> >
>>>> >         And have the added benefit of making the fields in the
>>>> __init__
>>>> >         call entirely explicit.
>>>> >
>>>> >         So, I propose the addition of a keyword_only flag to the
>>>> >         @dataclass decorator that renders the __init__ method using
>>>> >         keyword only arguments:
>>>> >
>>>> >         @dataclass(keyword_only=True)
>>>> >         class Bar(Foo):
>>>> >              other_field: int
>>>> >
>>>> >         --George Leslie-Waksman
>>>> >
>>>> >         _______________________________________________
>>>> >         Python-ideas mailing list
>>>> >         Python-ideas at python.org <mailto:Python-ideas at python.org>
>>>> >         https://mail.python.org/mailman/listinfo/python-ideas
>>>> >         Code of Conduct: http://python.org/psf/codeofconduct/
>>>> >
>>>> >
>>>> >
>>>> >
>>>> > _______________________________________________
>>>> > Python-ideas mailing list
>>>> > Python-ideas at python.org
>>>> > https://mail.python.org/mailman/listinfo/python-ideas
>>>> > Code of Conduct: http://python.org/psf/codeofconduct/
>>>> >
>>>>
>>>>
>>> _______________________________________________
>>> Python-ideas mailing list
>>> Python-ideas at python.org
>>> https://mail.python.org/mailman/listinfo/python-ideas
>>> Code of Conduct: http://python.org/psf/codeofconduct/
>>>
>>>
>>
>>
>> --
>> --Guido van Rossum (python.org/~guido)
>>
>


-- 
--Guido van Rossum (python.org/~guido)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20180129/c49d96e2/attachment-0001.html>


More information about the Python-ideas mailing list