해쟈스토리

dataclasses 모듈 본문

파이썬/클린코드

dataclasses 모듈

해쟈 2024. 6. 17. 16:09
728x90
반응형

파이썬에는객체의 값을 초기화하는 코드가 있다. __init__  메서드가 객체에서 필요한 모든 속성을 파라미터로 받은 다음 내부 변수에 할당하는 것이다. 일반적으로 다음과 같은 형태로 작성한다.

def __init__(self, x, y, ...):
	self.x = x
    self.y = y

dataclasses 모듈은 @dataclass 데코레이터를 제공한다. 이 데코레이터를 클래스에 적용하면 모든 클래스의 속성에 대해서 마치 __init__ 메서드에서 정의한 것처럼 인스턴스 속성으로 처리한다. 즉, @dataclass를 사용하면 __init__ 메서드를 자동으로 생성하므로 구현할 필요가 없다.

from dataclasses import dataclass

@dataclass
class Person:
	name:str
	age:int

 

위에서 정의한 name, age가 속성이 있는데 dataclass에서 주의할 점은 이 속성이  클래스의 인스턴스가 아닌 클래스 자체의 속성이기 때문에 공유되어서 mutable 객체(ex. 리스트)는 할당할 수 없다.

from dataclasses import dataclass


@dataclass(unsafe_hash=True)
class User:
    name: str
    admin: bool = False
    id: int = 1


user1 = User('홍길동', False)
admin1 = User('관리자', True)

print(user1.id)
print(admin1.id)

위와 같이 id에 기본값으로 1을 주고 user1, admin1의 id를 출력해보면 둘다 1이 나온다.  당연한 이야기라고 생각할 수 있다. 초기값을 1로 줬으니까... 그러나 리스트라고 생각해보자. 빈 리스트를 초기값으로 주게 되면 user1, admin1 모두 동일한 주소의 list의 값을 가지고 있게 된다. 그래서 user1에서 해당 리스트의 요소를 변경하게 되면 admin1도 변경된다.

02-8 자료형의 값을 저장하는 공간, 변수 - 점프 투 파이썬 (wikidocs.net)

 

02-8 자료형의 값을 저장하는 공간, 변수

지금부터 설명할 내용은 프로그래밍 초보자가 쉽게 이해하기 어려울 수 있는 부분이므로 당장 이해되지 않는다면 그냥 읽고 지나가도 된다. 파이썬에 대해서 공부하다 보면 자연스럽게 알…

wikidocs.net

 

 

그렇다면 빈 리스트를 넣어줘야할 때는어떻게 할 수 있을까? 그래서 dataclass에서는 field 객체를 제공한다.

from dataclasses import dataclass, field

@dataclass
class Person:
    names: list = field(default_factory=list)

person1 = Person()
person2 = Person()

person1.names.append("Alice")
person1.names.append("Bob")

print(person1.names)  # 출력: ['Alice', 'Bob']
print(person2.names)  # 출력: []

 

위와 같이 field 객체를 사용하여 default_factory 파라미터에 list함수를 전달한다.(이때 callable 객체를 전달해야한다.)

 

그러면 초기화 직후 유효성 검사를 하고  싶으면 어떻게 해야할까? __init__ 메서드가 없는데.

이럴때를 위해 __post_init__라는 데코레이터를 제공한다.

from dataclasses import dataclass, field, post_init

@dataclass
class Person:
    name: str
    age: int = field(default_factory=int)

    @post_init
    def __validate__(self):
        if not isinstance(self.name, str):
            raise ValueError("이름은 문자열이어야 합니다.")
        if not isinstance(self.age, int):
            raise ValueError("나이는 정수여야 합니다.")

person = Person("Alice", 25)

 

초기화 전에 유효성 검사를 할 수도 있다. 초기화를 한 뒤에 유효성 검사나 초기화 전에 하는 거나 똑같은거 같은데 왜 두가지 방식을 제공하는 걸까? 사실 위 코드에서는 별차이를 못 느낄 수 있다. __post_init__ 메서드를 사용하는 것은 기본 초기화 프로세스를 존중하면서 추가적인 논리를 수행하고자 할 때 유용하다. 모든 필드가 설정된 후의 상태를 기반으로 두 필드의 관계를 검증하거나 할 때 사용할 수 있다. 

 

아래는 property 데코레이터를 사용해서 초기화 전에  유효성을 검사하는 예제이다.

 

from dataclasses import dataclass, field


@dataclass
class Person:
    name: str
    _age: int = field(init=False, default_factory=int)         # init=False는 해당 필드가 자동으로 __init__ 메서드의 매개변수로 포함되지 않도록 지정하는 옵션 

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age must be a positive integer")
        self._age = value


try:
    person = Person("Alice", -1)
except ValueError as e:
    print(e)

 

dataclass는 __init__를 안해도 되지만 property 데코레이터를 사용하여 set할 때 self.age에 직접 할당하면 프로퍼티 세터가 계속 호출되어 무한 재귀에 빠질 수 있다. (setter가 = 일 때 동작하는건데 세터 함수에 self.age = value를 넣어버리면 또 세터가 불리게 됨)

그래서 dataclass 속성에서는 내부변수로 사용할 _age를 넣고 init로 age가 할당되면 세터가 동작해서 _age에 할당해주고 getter로 age에 접근하려고 하면 _age의 값을 return하도록 하는것이다. 

field 객체에서 init=False를 하는 것은 init 메서드의 매개변수에 해당 필드를 포함하지 않겠다는 의미이다. 내부 변수 _age는 외부로 부터 받는 값으로 할당되는것이 아닌 setter에 의해 설정되어야 하기 때문에 init=False를 해준다.

 

데이터 클래스는 __init__ 메서드에서 별도의 처리를 하거나, 유효성 검사가 엄격하게 필요하지 않는 경우에 사용하는 것이 적합하다.  데이터 컨테이너나 래퍼(wrapper) 클래스의 용도로 사용되는 모든 경우에 데이터 클래스가 유용할 것이다.

즉, 네임드 튜플이나 네임스페이스의 대안으로 적절하다. 

 

네임드튜플은 collections 모듈에 포함된 함수로 다음과 같이 사용 가능하다.

from collections import namedtuple

Person = namedtuple('Person', 'name, age')
user1 = Person('홍길동', 35)

print(user1.age)

 

user1.age로 접근하는 것은 데이터클래스나 namedTuple이나 똑같다. 그런데 왜 데이터 클래스를 쓰는 걸까? 데이터 클래스에서는 명시적으로 타입을 선얼할 수 있고, 필드 객체의 기본값을 설정할 수 있다. 또한 유효성 검사등도 가능하다. 또한 손쉽게 dict타입으로 속성과 값을 반환할 수 있다. (asdict()함수)

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

person = Person("Alice", 25)
person_dict = person.asdict()
print(person_dict)  # 출력: {'name': 'Alice', 'age': 25}

 

 

 

여기까지 파이썬에서 기능이 거의 없거나 하나도 없는, 필드들의 컬렉션인 간단한 클래스를 구축하는 몇 가지 방법을 알아보았다. 아래 다시 요약해 본다.

  • collections.namedtuple : 가장 간단한 방법 (available since Python 2.6)
  • typing.NamedTuple : 필드에 타입 힌트(type hint)를 요구하는 다른 방법 (since Python 3.5, with class syntax added in Python 3.6)
  • @dataclasses.dataclass : 위의 두 방법보다 더 많은 커스터마이즈를 할 수 있는 클래스 데코레이터. 많은 옵션들을 추가할 수 있음(since Python 3.7)

 

참조 : 파이썬의 데이터 클래스 사용법 (dataclasses 모듈) | Engineering Blog by Dale Seo

 

파이썬의 데이터 클래스 사용법 (dataclasses 모듈)

Engineering Blog by Dale Seo

www.daleseo.com

[Python] Data Class Builders (tistory.com)

 

728x90
반응형

'파이썬 > 클린코드' 카테고리의 다른 글

결합도  (0) 2024.08.19
방어적 프로그래밍  (0) 2024.08.19
Property  (0) 2024.06.14
파이썬에서의 밑줄  (2) 2024.06.14
매직메소드(__enter__와 __exit__, 그리고 contextlib)  (0) 2024.06.13