dataclass 데코레이터를 활용한 python Class 개발하기

NewNewDaddy·2024년 12월 10일
0

PYTHON

목록 보기
8/9
post-thumbnail

0. INTRO

  • 파이썬에서 Class는 개발에 있어 굉장히 중요한 요소이다. 이제는 익숙해져서 그러려니 하면서 사용하는데 처음에는 코드를 굳이 왜 이렇게 만들었을까? 싶은 부분이 있었다. 바로 __init__ 함수였는데, self 라는 개념의 등장과 함께 Class를 더 학습하기 싫게 만드는 장벽같은 느낌을 나에게 주었다.
  • 파이썬의 내장 라이브러리인 dataclasses에서 지원하는 dataclass 메소드는 Class의 데코레이터로 사용되어 Class 코드를 조금 더 간결하고 직관적이게 생성할 수 있도록 도와주는 도구이다.
  • 생성자(__init__), 문자열 표현(__repr__), 비교 메서드(__eq__, __lt__ 등) 등을 자동으로 구현하여 간결하고 효율적인 Class 코드를 작성할 수 있게 해주기도 한다.

1. 활용 코드

1) __init__ 생성자 함수 생략 가능

  • 생성하려고 하는 Class에 @dataclass 데코레이터만 추가해주면 dataclasses 라이브러리 기능을 활용해 Class 개발이 가능하다.

  • 일반적인 Class 구성과 비교했을 때 가장 큰 차이점 중 하나는 Class 처음에 나오는 __init__ 메소드에 대한 생략이 가능하다는 것이다. 개인적으로는 dataclass 활용의 가장 큰 목적 중 하나라고 생각하는데 이 것으로 인해 Class 코드가 훨씬 더 깔끔해보인다.

  • 일반 Class

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
  • dataclass 활용

    from dataclasses import dataclass
    
    @dataclass
    class Person:
        name: str
        age: int

2) repr 메소드 자동 구현

  • __repr__ 메소드는 클래스 객체 호출시, 개발자가 쉽게 이해할 수 있도록 객체의 공식적인 문자열 표현을 반환해주는 함수이다. 일반 Class에서는 repr 메소드를 따로 구성해주어야 하지만 dataclass로 데코레이팅된 Class는 자동으로 해당 메소드가 구현이 된다.

  • 일반 Class

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __repr__(self):
            return f"{self.__class__.__qualname__}(name='{self.name}', age={self.age})"
    
    p1 = Person("John Doe", 30)
    print(p1)
    
    ---
    Person(name='John Doe', age=30)
  • dataclass 활용

    from dataclasses import dataclass
    
    @dataclass
    class Person:
        name: str
        age: int
    
    p1 = Person("John Doe", 30)
    print(p1)
    
    ---
    Person(name='John Doe', age=30)
  • 이 외에도 __eq__ 메소드도 생략이 가능하기 때문에 아래 비교 그림과 같이 코드가 간결해진다.

3) 클래스 속성에 기본값 지정하기

  • Class 객체가 받는 매개변수들의 기본값을 지정해줄 수 있다.

  • 일반 Class

    class Person:
        def __init__(self, name='Chris', age=25):
            self.name = name
            self.age = age
  • dataclass 활용

    • str, int, float, bool 등의 불변(immutable) 객체에 대해서만 아래와 같이 단순 선언이 가능하다.
    from dataclasses import dataclass
    
    @dataclass
    class Person:
        name: str = 'Chris'
        age: int = 30

4) 최초값 고정 설정

  • @dataclass(frozen=True) 옵션을 활용하여 객체 생성시 가지는 값에 대한 수정이 불가능하도록 강제할 수 있다.

    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Person:
        name: str
        age: int
    
    p1 = Person("John Doe", 30)
    
    p1.age = 40 ## 객체에 할당된 값을 수정하려고 하면 에러 발생!

5) 가변 객체(mutable object)에 대한 기본값 설정

  • 가변 객체(mutable object)란 생성된 이후에도 내부 상태나 데이터를 변경할 수 있는 객체를 의미한다. Python의 list, dict, set 등이 이에 해당될 수 있다.

  • Python Class에서 기본값으로 가변 객체를 설정하면, 새로운 객체들을 생성하더라도 이 기본값이 모든 객체에 공유된다.

    class Person:
        def __init__(self, name='Unknown', age=20, tags=[]):
            self.name = name
            self.age = age
            self.tags = tags
    
    p1 = Person()
    p2 = Person()
    
    p1.tags.append("developer")
    
    print(p1.tags)  # ['developer']
    print(p2.tags)  # ['developer'] (p1과 공유됨)
  • dataclass는 기본적으로 가변 객체에 대해서 단순 디폴트 선언을 금지하고 있기 때문에 아래와 같이 ags: list = []이런식으로 선언하게 되면 오류가 발생한다.

    @dataclass
    class Person:
        name: str = "Unknown"
        age: int = 20
        tags: list = []  # 오류 발생!
  • efault_factory옵션을 활용하면 Class의 객체가 생성될 때마다 새로운 가변 객체를 생성하여 객체들간에 리소스가 공유되는 문제를 방지할 수 있다.

    from dataclasses import field
    
    @dataclass
    class Person:
        name: str = "Chris"
        age: int = field(default=20) # 불변 객체에 대해서도 기본값 선언
        tags: list = field(default_factory=list) # 가변 객체에 대한 기본값 선언
    
    p1 = Person()
    p2 = Person()
    
    p1.tags.append("developer")
    
    print(p1.tags)
    print(p2.tags)

6) Keyword Only 옵션

  • Class 객체 생성시, 특정 매개 변수가 반드시 key-value 형태로 정의되도록 강제하는 옵션이다.

  • 클래스의 속성에 field(kw_only=True) 옵션을 주어 설정 가능하다.

    from dataclasses import dataclass, field
    
    @dataclass
    class Person:
        name: str
        age: int = field(kw_only=True)
    
    # age에 kw_only 옵션이 설정되어 있기 때문에 단순히 value만 넘겨주면 에러가 발생한다.
    p1 = Person("John", 20) # 에러 발생 
    
    p2 = Person("John", age=20) # 정상 실행

7) __post_init__ 옵션

  • init 함수 실행 후 호출되며, 모든 필드가 초기화된 이후에 __post_init__ 메소드가 호출되어 Class내 존재하는 변수나 메소드들에 대해 추가적인 작업 수행이 가능하다.

  • __post_init__ 이전에는 단순히 객체 변수에 대한 선언 역할만 할 수 있기 때문에 기존 Class의 __init__ 함수 내에서 특정 조건 매칭 혹은 계산이 수행되는 등의 작업 행위가 수행되었다면 dataclass에서는 __post_init__ 함수 이하에 위치시킨다.

    from dataclasses import dataclass
    
    @dataclass
    class Person:
        name: str
        age: int
    
        def __post_init__(self):
            if self.age < 0:
                raise ValueError("Age cannot be negative")
    
    person = Person(name="Alice", age=25)
    print(person)

8) Class 객체 생성시 init 여부 설정

  • 특정 필드를 init 메소드의 매개변수에서 제외하는 설정으로, 객체 생성시 초기화되지 않기 때문에 init 이후 Class 코드 내에서 직접 설정해주어야 한다.

  • 제외할 변수에 field(init=False) 옵션을 주어 설정이 가능하다.

    from dataclasses import dataclass, field
    
    @dataclass
    class Person:
        name: str
        age: int
        is_adult: bool = field(init=False)  # __init__에서 제외
    
        def __post_init__(self):
            # __post_init__에서 is_adult 값 설정
            self.is_adult = self.age >= 18
    
    # 객체 생성
    person = Person(name="John", age=20)
    print(person)  # Output: Person(name='John', age=20, is_adult=True)

9) 동적인 Class 생성

  • make_dataclass 메소드를 활용하여 Class를 함수를 활용하여 동적으로 생성해줄 수 있다.

    from dataclasses import dataclass
    
    @dataclass
    class Person:
        name: str
        age: int
    
        def add_age(self):
            res = self.age + 10
            return res
    
    person = Person(name="Alice", age=25)
  • 위의 python Class를 아래와 같이 make_dataclass 함수를 사용하여 생성할 수 있다

    from dataclasses import make_dataclass
    
    Person = make_dataclass(
        cls_name='Person',
        fields=[("name", str), ("age", int)],
        namespace=dict(
            add_age = lambda self: self.age + 10
                )
            )
    
    Person("hyunsoo", 10)

10) 클래스 상속

  • 일반적으로 Class 상속시 super().__init__()을 통해 자식 Class에서 부모 Class의 속성이나 메소드들을 받아오게 된다.

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def greet(self):
            print(f"Hello, my name is {self.name} and I am {self.age} years old.")
    
    # Employee 클래스에서 Person 클래스를 상속
    class Employee(Person):
        def __init__(self, name, age, id, department):
            super().__init__(name, age)  # 부모 클래스(Person)의 생성자 호출
            self.id = id
            self.department = department
    
        def work(self):
            print(f"Worker ID is {self.id} and department is {self.department}")
            
    employee = Employee("Alice", 30, 1001, "HR")
    employee.greet()  # 부모 클래스의 메서드 호출
    employee.work()   # 자식 클래스의 메서드 호출
  • @dataclass를 활용하면 super().__init__()을 명시적으로 호출할 필요 없이 자동적으로 모든 속성을 초기화하여 자식 Class에서 부모 Class의 속성과 메소드를 받아올 수 있다.

    @dataclass
    class Person:
        name: str
        age: int
    
        def greet(self):
            print(f"Hello, my name is {self.name} and I am {self.age} years old.")
    
    @dataclass
    class Employee(Person):
        id: int
        department: str
    
        def work(self):
            print(f"Worker ID is {self.id} and department is {self.department}")
            
    employee = Employee("Alice", 30, 1001, "HR")
    employee.greet()  # 부모 클래스의 메서드 호출
    employee.work()   # 자식 클래스의 메서드 호출

11) 객체 및 Class 내용 확인

1. dataclass로 관리되는 Class에 대한 metadata 출력

help(employee)

---출력내용
Help on Employee in module __main__ object:

class Employee(Person)
 |  Employee(name: str, age: int, id: int, department: str) -> None
 |  
 |  Employee(name: str, age: int, id: int, department: str)
 |  
 |  Method resolution order:
 |      Employee
 |      Person
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __eq__(self, other)
 |      Return self==value.
 |  
 |  __init__(self, name: str, age: int, id: int, department: str) -> None
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __repr__(self)
 |      Return repr(self).
 |  
 |  work(self)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __annotations__ = {'department': <class 'str'>, 'id': <class 'int'>}
 |  
 |  __dataclass_fields__ = {'age': Field(name='age',type=<class 'int'>,def...
 |  
 |  __dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,or...
 |  
 |  __hash__ = None
 |  
 |  __match_args__ = ('name', 'age', 'id', 'department')
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Person:
 |  
 |  greet(self)
 |      # 부모 클래스의 메서드
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Person:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

2. 객체가 가진 속성들 출력

from dataclasses import asdict

## 방법 1
employee.__dict__

## 방법 2
asdict(employee)

---출력내용
{'name': 'Alice', 'age': 30, 'id': 1001, 'department': 'HR'}

3. 참고 자료

profile
데이터 엔지니어의 작업공간 / #PYTHON #CLOUD #SPARK #AWS #GCP #NCLOUD

0개의 댓글