__init__
함수였는데, self
라는 개념의 등장과 함께 Class를 더 학습하기 싫게 만드는 장벽같은 느낌을 나에게 주었다.dataclasses
에서 지원하는 dataclass
메소드는 Class의 데코레이터로 사용되어 Class 코드를 조금 더 간결하고 직관적이게 생성할 수 있도록 도와주는 도구이다.__init__
), 문자열 표현(__repr__
), 비교 메서드(__eq__
, __lt__
등) 등을 자동으로 구현하여 간결하고 효율적인 Class 코드를 작성할 수 있게 해주기도 한다.__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
__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__
메소드도 생략이 가능하기 때문에 아래 비교 그림과 같이 코드가 간결해진다.
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
@dataclass(frozen=True)
옵션을 활용하여 객체 생성시 가지는 값에 대한 수정이 불가능하도록 강제할 수 있다.
from dataclasses import dataclass
@dataclass(frozen=True)
class Person:
name: str
age: int
p1 = Person("John Doe", 30)
p1.age = 40 ## 객체에 할당된 값을 수정하려고 하면 에러 발생!
가변 객체(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)
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) # 정상 실행
__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)
특정 필드를 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)
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)
일반적으로 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() # 자식 클래스의 메서드 호출
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)
from dataclasses import asdict
## 방법 1
employee.__dict__
## 방법 2
asdict(employee)
---출력내용
{'name': 'Alice', 'age': 30, 'id': 1001, 'department': 'HR'}