如何在冻结的数据类自定义__init__方法中设置属性?

时间:2019-09-11 17:11:32

标签: python python-3.x python-3.7 python-dataclasses

我正在尝试构建一个定义架构的@dataclass,但实际上并未使用给定成员实例化。 (基本上,我出于其他目的劫持了方便的@dataclass语法)。这几乎可以满足我的要求:

@dataclass(frozen=True, init=False)
class Tricky:
    thing1: int
    thing2: str

    def __init__(self, thing3):
        self.thing3 = thing3

但是我在FrozenInstanceError方法中得到了一个__init__

dataclasses.FrozenInstanceError: cannot assign to field 'thing3'

我需要frozen=True(用于散列)。有什么方法可以在冻结的__init__上的@dataclass中设置自定义属性?

4 个答案:

答案 0 :(得分:3)

  

我需要frozen=True(出于哈希性考虑)。

没有严格的要求冻结类以使其具有可哈希性。您可以选择不从代码中的任何位置更改属性,而设置unsafe_hash=True

但是,您应该真正将thing3声明为字段,并且不要使用自定义的__init__

from dataclasses import dataclass, field
from typing import Any

@dataclass(unsafe_hash=True)
class Tricky:
    thing1: int = field(init=False)
    thing2: str = field(init=False)
    thing3: Any

    def __post_init__(self):
        self.thing1 = 42
        self.thing2 = 'foo'

此处thing1thing2设置了init=False,因此它们不会传递给__init__方法。然后,您可以使用__post_init__()方法设置它们。

请注意,这现在要求您不冻结类,否则就不能在自定义的{{1中设置thing1thing2 }},而不是__init__

演示:

__post_init__

如果您只需要一个模式定义,则根本不需要数据类。您可以从任何类获得类注释; raw annotationstyping.get_type_hints()

答案 1 :(得分:2)

问题是the default __init__ implementation uses object.__setattr__() with frozen classes并通过提供您自己的实现,您也必须使用它,这会使您的代码很hacky:

@dataclass(frozen=True, init=False)
class Tricky:
    thing1: int
    thing2: str

    def __init__(self, thing3):
        object.__setattr__(self, "thing3", thing3)

不幸的是,python没有提供使用默认实现的方法,因此我们不能简单地做类似的事情:

@dataclass(frozen=True, init=False)
class Tricky:
    thing1: int
    thing2: str

    def __init__(self, thing3, **kwargs):
        self.__default_init__(DoSomething(thing3), **kwargs)

但是,通过我们可以很容易地实现该行为:

def dataclass_with_default_init(_cls=None, *args, **kwargs):
    def wrap(cls):
        # Save the current __init__ and remove it so dataclass will
        # create the default __init__.
        user_init = getattr(cls, "__init__")
        delattr(cls, "__init__")

        # let dataclass process our class.
        result = dataclass(cls, *args, **kwargs)

        # Restore the user's __init__ save the default init to __default_init__.
        setattr(result, "__default_init__", result.__init__)
        setattr(result, "__init__", user_init)

        # Just in case that dataclass will return a new instance,
        # (currently, does not happen), restore cls's __init__.
        if result is not cls:
            setattr(cls, "__init__", user_init)

        return result

    # Support both dataclass_with_default_init() and dataclass_with_default_init
    if _cls is None:
        return wrap
    else:
        return wrap(_cls)

然后

@dataclass_with_default_init(frozen=True)
class DataClass:
    value: int

    def __init__(self, value: str):
        # error:
        # self.value = int(value)

        self.__default_init__(value=int(value))

更新:我打开了this错误,希望在3.9之前实现。

答案 2 :(得分:1)

事实证明数据类没有提供您正在寻找的功能。但是 Attrs 确实:

from attr import attrs, attrib


@attrs(frozen=True)
class Name:
    name: str = attrib(converter=str.lower)

类似问题的相同答案:见https://stackoverflow.com/a/64695607/3737651

答案 3 :(得分:0)

@Shmuel H.发布的内容对我不起作用,但仍然引发FrozenInstanceError

这对我有用:

我在这里正在做的事情是在init中接受一个值,并检查它是否与strptime函数中定义的格式兼容,如果是的话,我将为其分配,否则将打印该值。例外

@dataclass(frozen=True)
class Birthday:
    value: InitVar[str]
    date: datetime = field(init=False)

    def __post_init__(self, value: str):
        try:
            self.__dict__['date'] = datetime.strptime(value, '%d/%m/%Y')
        except Exception as e:
            print(e)