programing

frozen=True일 때 __post_init__에서 데이터 클래스 필드의 값을 설정하는 방법은 무엇입니까?

iphone6s 2023. 7. 21. 21:25
반응형

frozen=True일 때 __post_init__에서 데이터 클래스 필드의 값을 설정하는 방법은 무엇입니까?

고정 데이터 클래스를 생성하려고 하는데 다음 값을 설정하는 데 문제가 있습니다.__post_init__필드 값을 설정하는 방법이 있습니까?init param순식간에dataclass를 할 때frozen=True세팅?

RANKS = '2,3,4,5,6,7,8,9,10,J,Q,K,A'.split(',')
SUITS = 'H,D,C,S'.split(',')


@dataclass(order=True, frozen=True)
class Card:
    rank: str = field(compare=False)
    suit: str = field(compare=False)
    value: int = field(init=False)
    def __post_init__(self):
        self.value = RANKS.index(self.rank) + 1
    def __add__(self, other):
        if isinstance(other, Card):
            return self.value + other.value
        return self.value + other
    def __str__(self):
        return f'{self.rank} of {self.suit}'

그리고 이것이 그 흔적입니다.

 File "C:/Users/user/.PyCharm2018.3/config/scratches/scratch_5.py", line 17, in __post_init__
    self.value = RANKS.index(self.rank) + 1
  File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'value'

생성된 메서드와 동일한 작업을 사용합니다.object.__setattr__.

def __post_init__(self):
    object.__setattr__(self, 'value', RANKS.index(self.rank) + 1)

제가 거의 모든 수업에서 사용하는 해결책은 추가적인 생성자를 수업 방법으로 정의하는 것입니다.

주어진 예를 바탕으로 다음과 같이 다시 작성할 수 있습니다.

@dataclass(order=True, frozen=True)
class Card:
    rank: str = field(compare=False)
    suit: str = field(compare=False)
    value: int

    def __post_init__(self) -> None:
        if not is_valid_rank(self.rank):
            raise ValueError(f"Rank {self.rank} of Card is invalid!")

    @classmethod
    def from_rank_and_suite(cls, rank: str, suit: str) -> "Card":
        value = RANKS.index(self.rank) + 1
        return cls(rank=rank, suit=suit, value=value)

이로써 우리는 의지할 필요 없이 필요한 모든 자유를 갖게 됩니다.__setattr__해킹과 같은 원하는 엄격함을 포기할 필요 없이.frozen=True.

변환 사용

동결된 물체는 변경해서는 안 됩니다.하지만 가끔 필요성이 생길 수도 있습니다.그것에 대해 받아들여진 대답은 완벽하게 작동합니다.이에 접근하는 또 다른 방법은 변경된 값으로 인스턴스를 반환하는 것입니다.어떤 경우에는 과잉 살상이 될 수도 있지만, 선택 사항입니다.

from copy import deepcopy

@dataclass(frozen=True)
class A:
    a: str = ''
    b: int = 0

    def mutate(self, **options):
        new_config = deepcopy(self.__dict__)
        # some validation here
        new_config.update(options)
        return self.__class__(**new_config)

다른 접근법

값을 모두 또는 여러 개 설정하려면 다음을 호출합니다.__init__ 다시마로으속 안에서.__post_init__비록 사용 사례가 많지는 않지만요.

다음 예제는 가능성을 입증하기 위한 것일 뿐 실용적이지 않습니다.

from dataclasses import dataclass, InitVar


@dataclass(frozen=True)
class A:
    a: str = ''
    b: int = 0
    config: InitVar[dict] = None

    def __post_init__(self, config: dict):
        if config:
            self.__init__(**config)

다음 전화

A(config={'a':'a', 'b':1})

항복할 것입니다

A(a='a', b=1)

실수 없이이것은 python 3.7 및 3.9에서 테스트되었습니다.

물론, 다음을 사용하여 직접 구성할 수 있습니다.A(a='hi', b=1)그러나 json 파일에서 구성을 로드하는 등 다른 용도가 있을 수 있습니다.

보너스: 훨씬 더 미친 사용법

A(config={'a':'a', 'b':1, 'config':{'a':'b'}})

항복할 것입니다

A(a='b', b=1)

캐시된 속성을 사용하여 개체 변환을 방지하는 솔루션

이것은 @Anna Giasson 답변의 단순화된 버전입니다.

동결된 데이터 클래스는 functools 모듈의 캐싱과 함께 잘 작동합니다.를 사용하는 대신dataclass필드, 당신은 정의할 수 있습니다.@functools.cached_property속성을 처음 조회할 때만 평가되는 주석이 달린 메서드입니다.원본 예제의 최소 버전은 다음과 같습니다.

from dataclasses import dataclass
import functools

@dataclass(frozen=True)
class Card:
    rank: str

    @functools.cached_property
    def value(self):
        # just for demonstration:
        # this gets printed only once per Card instance
        print("Evaluate value")
        return len(self.rank)

card = Card(rank="foo")

assert card.value == 3
assert card.value == 3

실제로 평가 비용이 저렴할 경우 비캐시를 사용할 수도 있습니다.@property

이것은 동결된 데이터 클래스의 의도를 '해킹'하는 것처럼 약간 느껴지지만, 잘 작동하고 post_init 메서드 내에서 동결된 데이터 클래스를 수정하는 데 깨끗합니다.이 데코레이터는 어떤 방법으로든 사용할 수 있습니다(데이터 클래스가 동결될 것으로 예상되는 경우에는 무섭습니다). 따라서 저는 이 데코레이터가 부착된 함수 이름이 'post_init'이어야 한다고 주장하여 보상했습니다.

클래스와 별도로 클래스에서 사용할 장식자를 작성합니다.

def _defrost(cls):
    cls.stash_setattr = cls.__setattr__
    cls.stash_delattr = cls.__delattr__
    cls.__setattr__ = object.__setattr__
    cls.__delattr__ = object.__delattr__

def _refreeze(cls):
    cls.__setattr__ = cls.stash_setattr
    cls.__delattr__ = cls.stash_delattr
    del cls.stash_setattr
    del cls.stash_delattr

def temp_unfreeze_for_postinit(func):
    assert func.__name__ == '__post_init__'
    def wrapper(self, *args, **kwargs):
        _defrost(self.__class__)
        func(self, *args, **kwargs)
        _refreeze(self.__class__)
    return wrapper

그런 다음, 고정된 데이터 클래스 에서 post_init 메서드를 간단히 장식하십시오.

@dataclasses.dataclass(frozen=True)
class SimpleClass:
    a: int

    @temp_unfreeze_for_postinit
    def __post_init__(self, adder):
        self.b = self.a + adder

제가 우연히 이 문제를 발견했을 때 저만의 솔루션으로 의견을 제시했지만 제 애플리케이션에 적합한 솔루션이 하나도 없었습니다.

여기서 OP와 마찬가지로 처음에 post_init 메서드로 만들려고 했던 속성은 bit_mask 속성입니다.

속성이 데이터 클래스의 다른 속성과 마찬가지로 정적/불변의 속성이 되기를 원했기 때문에 functools에서 cache_property 데코레이터를 작동하도록 했습니다.

create_bitmask 함수는 내 코드의 다른 곳에 정의되어 있지만 데이터 클래스 인스턴스의 다른 속성에 따라 다르다는 것을 알 수 있습니다.

바라건대, 다른 누군가가 이것이 도움이 될 수 있기를 바랍니다.

from dataclasses import dataclass
from functools import cached_property

@dataclass(frozen=True)
class Register:
    subsection: str
    name: str
    abbreviation: str
    address: int
    n_bits: int
    _get_method: Callable[[int], int]
    _set_method: Callable[[int, int], None]
    _save_method: Callable[[int, int], None]

    @cached_property
    def bit_mask(self) -> int:
        # The cache is used to avoid recalculating since this is a static value
        # (hence max_size = 1)
        return create_bitmask(
            n_bits=self.n_bits,
            start_bit=0,
            size=self.n_bits,
            set_val=True
            )

    def get(self) -> int:
        raw_value = self._get_method(self.address)
        return raw_value & self.bit_mask

    def set(self, value: int) -> None:
        self._set_method(
            self.address,
            value & self.bit_mask
            )

    def save(self, value: int) -> None:
        self._save_method(
            self.address,
            value & self.bit_mask
            )

피터 바메틀러가 제안한 돌연변이를 피하는 것이 제가 그런 경우에 하는 경향이 있습니다.frozen=True 기능과 훨씬 더 일치하는 느낌이 듭니다.참고로, 주문 =True__add_ 메서드는 카드 목록을 기준으로 점수를 정렬하고 계산하고 싶다고 생각하게 했습니다.

가능한 접근 방식은 다음과 같습니다.

from __future__ import annotations
from dataclasses import dataclass

RANKS = '2,3,4,5,6,7,8,9,10,J,Q,K,A'.split(',')
SUITS = 'H,D,C,S'.split(',')


@dataclass(frozen=True)
class Card:
    rank: str
    suit: str

    @property
    def value(self) -> int:
        return RANKS.index(self.rank) + 1

    def __lt__(self, __o: Card) -> bool:
        return self.value < __o.value

    def __str__(self) -> str:
        return f'{self.rank} of {self.suit}'

    @classmethod
    def score(cls, cards: list[Card]) -> int: 
        return sum(card.value for card in cards)


c1 = Card('A', 'H')
c2 = Card('3', 'D')

cards = [c1, c2]

Card.score(cards) # -> 15
sorted(cards) # -> [Card(rank='3', suit='D'), Card(rank='A', suit='H')]

점수를 매기는 논리는 클래스 방법일 필요는 없지만 카드의 값을 결정하는 논리도 클래스 안에 있기 때문에 괜찮습니다.

언급URL : https://stackoverflow.com/questions/53756788/how-to-set-the-value-of-dataclass-field-in-post-init-when-frozen-true

반응형