Dataclass-style object with mutable and immutable properties?

可紊 提交于 2020-05-16 07:47:25

问题


I have been playing around with dataclasses dynamically loaded with property names from a file and I am unable to find a way to create both 'frozen' and 'non-frozen' properties. I believe dataclasses only allow you to set all properites to frozen or non-frozen.

As of now, I create a frozen dataclass and add a mutable class as one of the properties which I can change as I go but I am not very happy with the readability of this approach.

Is there another pythonic dataclass people would recommend without needing to implement a class with the ability to set mutable/immutable properties?

import dataclasses

class ModifiableConfig:
    """There is stuff in here but you get the picture."""
    ...

config_dataclass = dataclasses.make_dataclass(
    'c',
    [(x, type(x), v) for x, v in config.items()] + [('var', object, ModifiableConfig())],
    frozen=True
)

However I would prefer the ability to choose which attributes are frozen and which are not. Making the need of adding an additional class to the dataclass obsolete. It may look like this:

config_dataclass_modifiable = dataclasses.make_dataclass(
            'c', [(x, type(x), v, True if 'modifiable' in x else False) for x, v in config.items()])

Notice the "True if 'modifiable' in x else False", I'm not saying this is how I would do it in the end but hopefully this helps understand my question better.


回答1:


The normal approach to tuning attribute handling is writing a custom __setattr__ method which allows you to override the default behavior for attribute assignments. Unfortunately, that method is also what dataclasses hooks into to enforce the frozen logic, which effectively locks the function from being altered any further by throwing TypeError: Cannot overwrite attribute __setattr__ in class ModifiableConfig as soon as you try to touch it.

As a consequence, there is no straight forward and simple solution to your problem that I can see. Your approach of delegating the mutable parts of a class to an inner object or dictionary is, in my opinion, not bad or un-pythonic at all, but if you're fine with dropping frozen from your requirements list and only want a partly-mutable dataclass, you can try using this bootleg-semi-frozen recipe here that updates the dataclass decorator with a flag semi that you can switch on to get the behavior you described:

from dataclasses import dataclass as dc
from traceback import format_stack

def dataclass(_cls=None, *, init=True, repr=True, eq=True, order=False,
              unsafe_hash=False, frozen=False, semi=False):

    def wrap(cls):
        # sanity checks for new kw
        if semi:
            if frozen:
                raise AttributeError("Either semi or frozen, not both.")
            if cls.__setattr__ != cls.mro()[1].__setattr__:
                raise AttributeError("No touching setattr when using semi!")

        # run original dataclass decorator
        dc(cls, init=init, repr=repr, eq=eq, order=order,
           unsafe_hash=unsafe_hash, frozen=frozen)

        # add semi-frozen logic
        if semi:
            def __setattr__(self, key, value):
                if key in self.__slots__:
                    caller = format_stack()[-2].rsplit('in ', 1)[1].strip()
                    if caller != '__init__':
                        raise TypeError(f"Attribute '{key}' is immutable!")
                super(type(self), self).__setattr__(key, value)
            cls.__setattr__ = __setattr__

        return cls

    # Handle being called with or without parens
    if _cls is None:
        return wrap
    return wrap(_cls)

I'm being brief here and only handle the cases that look like probable shortcomings to me. There are better ways to handle the wrapping so that the internals are more consistent, but it would blow this already complicated snippet up even more.

Given this new dataclass decorator, you can use it like this to define a dataclass with some immutable attributes and some mutable ones:

>>> @dataclass(semi=True)
... class Foo:
...     # put immutable attributes and __dict__ into slots 
...     __slots__ = ('__dict__', 'x', 'y')
...     x: int
...     y: int
...     z: int
...
>>> f = Foo(1, 2, 3)
>>> f        # prints Foo(x=1, y=2, z=3)
>>> f.z = 4  # will work
>>> f.x = 4  # raises TypeError: attribute 'x' is immutable!

You don't have to use __slots__ to separate the mutable from the immutable part, but it is convenient for a few reasons (such as being a meta-attribute that isn't part of the default dataclass repr) and felt intuitive to me.



来源:https://stackoverflow.com/questions/58532383/dataclass-style-object-with-mutable-and-immutable-properties

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!