Issue42742
This issue tracker has been migrated to GitHub,
and is currently read-only.
For more information,
see the GitHub FAQs in the Python's Developer Guide.
Created on 2020-12-25 16:32 by abrosimov.a.a, last changed 2022-04-11 14:59 by admin.
Messages (11) | |||
---|---|---|---|
msg383752 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-25 16:32 | |
I want to add `abc.Mapping` extension to `dataclasses.dataclass`. Motivation: 1. `asdict` makes a deep copy of the `dataclass` object. If I only want to iterate over the `field` attributes, I don't want to do a deep copy. 2. `dict(my_dataclass)` can be used as a `dict` representation of `my_dataclass` class without deep copying. 3. `myfunc(**my_dataclass)` looks better and is faster then `myfunc(**asdict(my_dataclass))`. 4. `len(my_dataclass) == len(asdict(my_dataclass))` is expected behavior. 5. `my_dataclass.my_field is my_dataclass['my_field']` is expected behavior. Looks like a prototype: from collections.abc import Mapping from dataclasses import dataclass, fields, _FIELDS, _FIELD @dataclass # `(mapping=True)` creates such a class: class MyDataclass(Mapping): a: int = 1 b: int = 2 # In `dataclasses._process_class`: # if `mapping` is `True`. # Make sure 'get', 'items', 'keys', 'values' is not in `MyDataclass` fields. def __iter__(self): return (f.name for f in fields(self)) def __getitem__(self, key): fields = getattr(self, _FIELDS) f = fields[key] if f._field_type is not _FIELD: raise KeyError(f"'{key}' is not a field of the dataclass.") return getattr(self, f.name) def __len__(self): return len(fields(self)) my_dataclass = MyDataclass(b=3) print(my_dataclass['a']) print(my_dataclass['b']) print(dict(my_dataclass)) print(dict(**my_dataclass)) Stdout: 1 3 {'a': 1, 'b': 3} {'a': 1, 'b': 3} Realisation: Updating the `dataclasses.py`: `dataclass`, `_process_class`, `_DataclassParams`. Set `mapping` argument to default `False`. Can this enhancement be accepted? |
|||
msg383755 - (view) | Author: Eric V. Smith (eric.smith) * ![]() |
Date: 2020-12-25 17:23 | |
You'd need to return a different class in order to add the collections.abc.Mapping base class. Currently, dataclasses by design always return the same class that's passed in. I'd suggest adding this as a stand-alone decorator. |
|||
msg383758 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-25 18:57 | |
Thanks for the answer, I agree. The implementation should be like this? from collections.abc import Mapping from dataclasses import dataclass, fields, _FIELDS, _FIELD class _DataclassMappingMixin(Mapping): def __iter__(self): return (f.name for f in fields(self)) def __getitem__(self, key): fields = getattr(self, _FIELDS) f = fields[key] if f._field_type is not _FIELD: raise KeyError(f"'{key}' is not a dataclass field.") return getattr(self, f.name) def __len__(self): return len(fields(self)) def dataclass_mapping(cls=None, **kwargs): def apply_dataclass(cls): dataclass_wrap = dataclass(**kwargs) return dataclass_wrap(cls) def check_mapping_attrs(cls): mapping_attrs = (i for i in dir(_DataclassMappingMixin) if i[0] != '_') for key in mapping_attrs: if hasattr(cls, key): raise AttributeError(f"'{key}' is the Mapping reserved attribute.") def apply_mapping(cls): return type(cls.__name__ + 'Mapping', (cls, _DataclassMappingMixin), {}) def wrap(cls): check_mapping_attrs(cls) cls_dataclass = apply_dataclass(cls) return apply_mapping(cls_dataclass) # See if we're being called as @dataclass or @dataclass(). if cls is None: # We're called with parens. return wrap # We're called as @dataclass without parens. return wrap(cls) @dataclass_mapping class MyDataclass: a: int = 1 b: int = 2 my_dataclass = MyDataclass(b='3') print(my_dataclass.__class__.__name__) print(my_dataclass['a']) print(my_dataclass['b']) print(dict(my_dataclass)) print(dict(**my_dataclass)) |
|||
msg383762 - (view) | Author: Eric V. Smith (eric.smith) * ![]() |
Date: 2020-12-25 21:16 | |
Something like that. You'd have to write some tests and try it out. |
|||
msg383764 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-25 21:23 | |
An alternative way: from collections.abc import Mapping from dataclasses import dataclass, fields, _FIELDS, _FIELD class DataclassMappingMixin(Mapping): def __iter__(self): return (f.name for f in fields(self)) def __getitem__(self, key): field = getattr(self, _FIELDS)[key] if field._field_type is not _FIELD: raise KeyError(f"'{key}' is not a dataclass field.") return getattr(self, field.name) def __len__(self): return len(fields(self)) @dataclass class MyDataclass(DataclassMappingMixin): a: int = 1 b: int = 2 my_dataclass = MyDataclass(a='3') print(my_dataclass.__class__.__mro__) print(my_dataclass.__class__.__name__) print(my_dataclass['a']) print(my_dataclass['b']) print(dict(my_dataclass)) print(dict(**my_dataclass)) print(fields(my_dataclass)) Result: (<class '__main__.MyDataclass'>, <class '__main__.DataclassMappingMixin'>, <class 'collections.abc.Mapping'>, <class 'collections.abc.Collection'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>, <class 'object'>) MyDataclass 3 2 {'a': '3', 'b': 2} {'a': '3', 'b': 2} (Field(name='a',type=<class 'int'>, ...), Field(name='b',type=<class 'int'>, ...)) |
|||
msg383766 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-25 21:39 | |
I think the second option looks better. More pythonic. No need to create new classes No typing hacks. Mixin can be easily expanded. Yes, I will do refactoring, typing, documentation and tests in PR. |
|||
msg383767 - (view) | Author: Eric V. Smith (eric.smith) * ![]() |
Date: 2020-12-25 21:42 | |
I don't think this belongs in dataclasses itself, at least not until it's been vetted widely. You might want to put it on PyPI first as a standalone project. |
|||
msg383770 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-25 22:03 | |
This Mixin only works with dataclass objects. And uses the private functionality of the dataclasses. So dataclasses.py is the right place for this. I think I can do enough tests. And I think that this is too little for a standalone project. |
|||
msg383771 - (view) | Author: Eric V. Smith (eric.smith) * ![]() |
Date: 2020-12-25 22:05 | |
I'm just warning you that I probably won't accept it. I haven't heard of any demand for this feature. You might want to bring it up on python-ideas if you want to generate support for the proposal. |
|||
msg383774 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-25 22:20 | |
Thanks for the good offer, I will definitely use it. |
|||
msg384026 - (view) | Author: Anton Abrosimov (abrosimov.a.a) * | Date: 2020-12-29 19:49 | |
Link to python-ideas thread: https://mail.python.org/archives/list/python-ideas@python.org/thread/XNXCUJVNOOVPAPL6LF627EOCBUUUX2DG/ |
History | |||
---|---|---|---|
Date | User | Action | Args |
2022-04-11 14:59:39 | admin | set | github: 86908 |
2020-12-29 19:49:18 | abrosimov.a.a | set | messages: + msg384026 |
2020-12-25 22:20:46 | abrosimov.a.a | set | messages: + msg383774 |
2020-12-25 22:05:14 | eric.smith | set | messages: + msg383771 |
2020-12-25 22:03:10 | abrosimov.a.a | set | messages: + msg383770 |
2020-12-25 21:42:57 | eric.smith | set | messages: + msg383767 |
2020-12-25 21:39:47 | abrosimov.a.a | set | messages: + msg383766 |
2020-12-25 21:23:56 | abrosimov.a.a | set | messages: + msg383764 |
2020-12-25 21:16:24 | eric.smith | set | messages: + msg383762 |
2020-12-25 18:57:35 | abrosimov.a.a | set | messages: + msg383758 |
2020-12-25 17:23:40 | eric.smith | set | assignee: eric.smith messages: + msg383755 nosy: + eric.smith |
2020-12-25 16:32:43 | abrosimov.a.a | create |