승상의 코딩 블로그

파이썬 - dataclass(데이터클래스) 본문

Python (파이썬)

파이썬 - dataclass(데이터클래스)

양승상 2022. 12. 31. 11:25

파이썬 3.7부터 사용되고 있다. 그 이전버전은 dataclass 라이브러리를 따로 설치해야 한다.

type hint 는 버전마다 조금씩 다르게 사용되므로 참고해주시기 바랍니다. 아래 코드는 파이썬 버전 3.10 으로 작성하였습니다. 

 

기존에는 데이터를 구조화하기 위해서는 아래와 같이 클래스로 작성하는 경우가 많았다.

직원을 구현하는 클래스를 구현해보자.

class Employee:
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName


print(Employee(firstName="Yang", lastName="SeungSang"))
# 출력값: <__main__.Employee object at 0x1031cb040>

dataclass 가 있기 전에는 __init__ 함수의 파라미터를 멤버 변수와 맵핑하는 작업을 해줘야했다.

dataclass 는 __init__ 에서의 맵핑작업 이외에도 반복적인 일들을 줄여준다. 

다른 반복적인 일로는 객체 출력값 구현이 있다. 일반적으로 객체를 출력하면 메모리의 주소값이 뜬다.

클래스를 출력할 때 조금 더 의미 있는 데이터를 만들기 위해서는 아래와 같이 __repr__ 함수를 구현해줘야 한다.

class Employee:
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName
        
    def __repr__(self):
    	return f"Employee is {self.firstName} {self.lastName}"
	

print(Employee(firstName="Yang", lastName="SeungSang"))
# Employee is Yang SeungSang

DataClass

여러 반복 잡업들을 모두 없애주고 다양한 기능을 쉽게 설정할 수 있는 것이 dataclass 이다. 

from dataclasses import dataclass

@dataclass
class Employee:
    firstName: str
    lastName: str

print(Employee(firstName="Yang", lastName="SeungSang"))
# 출력값: Employee(firstName='Yang', lastName='SeungSang')

클래스에다가 dataclass 데코레이터만 추가해주면 사용할 수 있다.

클래스 변수처럼 필드(변수)를 기입하고 타입만 적어주면 된다. (하지만 클래스 변수처럼 공유되지는 않는다.)

그러면 자동으로 __init__ 에서 수행하던 맵핑을 수행해주고, 멤버 변수를 기반으로 __repr__ 함수도 구현해 준다.

 

어떻게 사용할 수 있는지 조금더 알아보자.

기본값 설정

dataclass 에 필드가 정의되었지만 객체 생성시 파라미터를 넘겨주지 않으면 에러가 난다.

이런 경우, 기본값을 설정하여 사용할 수 있다.

from dataclasses import dataclass

@dataclass
class Employee:
    firstName: str
    lastName: str
    year: int = 1

print(Employee(firstName="Yang", lastName="SeungSang"))
# 출력값: Employee(firstName='Yang', lastName='SeungSang', year=1)

print(Employee(firstName="Yang", lastName="SeungSang", year=2))
# 출력값: Employee(firstName='Yang', lastName='SeungSang', year=2)

year(근무 년차) 를 Employee 클래스에 추가하고, Default 값은 1로 설정했다.

field(default_factory=list) : 리스트 선언

primitive type 인 int, str 과는 다르게, reference type 인 리스트는 다르게 선언해줘야한다.

(primitive 는 값 자체라고 생각하고, reference 는 주소값 이라고 생각하면 된다.)

reference type은 주소값 사용하기 때문에, 개별 객체가 독립적으로 그 값을 관리하지 못한다.

Employee 의 A, B 객체가 있다면, A에서 값을 바꿀 경우 B의 값도 바뀐다.

이를 해결하기 위해, dataclasses 라이브러리에서는 field 함수를 제공한다.

from dataclasses import dataclass, field

@dataclass
class Employee:
    firstName: str
    lastName: str
    year: int = 1
    skills: list[str] = field(default_factory=list)

print(Employee(firstName="Yang", lastName="SeungSang", skills=['MachinLearning']))
# 출력값 : Employee(firstName='Yang', lastName='SeungSang', year=1, skills=['MachinLearning'])

field 함수의 default_factory 파라미터에 list 를 넣어주면, Employee 를 구현한 각 객체는 모두 독립적인 리스트를 가지게 된다.

__post_init__()

코드를 짜다보면, 각 값을 받아서 새로운 데이터를 생성해야 할 때가 있다.

이 때, __post_init__ 함수를 사용한다. 말 그대로 __init__ 함수 이후에 자동으로 호출되는 함수이다. 

firstName 과 lastName을 붙여서 fullName을 만들어보자.

from dataclasses import dataclass, field

@dataclass
class Employee:
    firstName: str
    lastName: str
    fullName: str = field(init=False)
    year: int = 1
    skills: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.fullName = f"{self.firstName} {self.lastName}"

print(Employee(firstName="Yang", lastName="SeungSang",
      skills=['MachinLearning']).fullName)
# 출력값 : Employee(firstName='Yang', lastName='SeungSang', fullName='Yang SeungSang', year=1, skills=['MachinLearning'])

Employee 에 fullName 필드를 추가해줬다.

dataclass 는 초기화할 때, 각 필드의 값을 파라미터로 넘겨줘야한다고 했다.

하지만 fullName 은 다른 필드의 값을 통해 생성되는 값이므로, fullName 파라미터는 받을 필요가 없다.

이를 위해, field 의 init 파라미터를 False 로 설정해준다. 이제 초기화 때 파라미터를 받지 않아도 에러가 나지 않는다.

마지막으로, __post_init__ 에서 firstName 과 lastName 을 조합해서 fullName을 생성한다.

 

물론, fullName 자체를 필드로 선언하지 않고 __post_init__ 에서 처음 선언해도 된다.

그 경우 출력값에 fullName에 대한 정보가 나오지 않는다. 또한 코딩할 때, 필드가 한곳에 모여있지 않으므로 가독성 면에서도 떨어진다고 생각한다.

field(repr=False)

데이터가 많아진다면 객체 출력시 보고싶지 않은 정보도 존재하게 된다.

 

__post_init__ 에서 firstName 과 lastName 을 조합해서 fullName 을 정의하였다.

그렇다면, firstName 과 lastName 을 굳이 객체 출력시 보여줄 필요가 없을 것이다.

이 때, field(repr=False) 를 사용한다.

from dataclasses import dataclass, field

@dataclass
class Employee:
    firstName: str = field(repr=False)
    lastName: str = field(repr=False)
    fullName: str = field(init=False)
    year: int = 1
    skills: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.fullName = f"{self.firstName} {self.lastName}"

print(Employee(firstName="Yang", lastName="SeungSang",
      skills=['MachinLearning']))
# 출력값 : Employee(fullName='Yang SeungSang', year=1, skills=['MachinLearning'])

field(repr=False)를 설정하면, 설정한 필드는 출력값에서 보이지 않게 된다. 

@dataclass(Frozen=True)

데이터가 변하는지 변하지 않는지는 매우 중요하다. 변수의 값이 변하는지 고려할 필요가 없으면, 코드가 단순해지기 때문이다.

파이썬은 다른 언어와 달리 const 같은 개념이 없었다. (한번 정의한 변수의 값이 변경가능함)

dataclass 를 사용하면 const 용도로도 사용할 수 있다.

from dataclasses import dataclass, field

@dataclass(frozen=True)
class Employee:
    firstName: str = field(repr=False)
    lastName: str = field(repr=False)
    fullName: str = field(init=False)
    year: int = 1
    skills: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.fullName = f"{self.firstName} {self.lastName}" # << Error

employee = Employee(firstName="Yang", lastName="SeungSang",
                    skills=['MachinLearning'])

그러나, 위의 예제의 경우에는 @dataclass(frozen=True) 를 할 경우, __post_init__ 에서 fullName 을 정의하면서 에러가 발생한다.

fullName 변수를 초기화 이후(__init__) 할당하지 않는다면 코드는 정상적으로 동작한다.

팩토리 메소드 패턴

위의 상황을 해결하기 위해서 팩토리 메소드 패턴 개념을 활용하면 됩니다.

from dataclasses import dataclass, field

@dataclass(frozen=True)
class Employee:
    firstName: str = field(repr=False)
    lastName: str = field(repr=False)
    fullName: str
    year: int
    skills: list[str] = field(default_factory=list)

    @classmethod
    def hireEmployee(cls, firstName, lastName, year=1, skills=None):
        fullName = f"{firstName} {lastName}"
        if skills is None:
            skills = []
        return cls(firstName=firstName, lastName=lastName, fullName=fullName, year=year, skills=skills)

employee = Employee.hireEmployee(firstName="Yang", lastName="SeungSang",
                                 skills=['MachinLearning'])
print(employee)
# Employee(fullName='Yang SeungSang', year=1, skills=['MachinLearning'])

이해를 위해, 간단히만 적었습니다.

일단, Employee 에 hireEmployee라는 클래스함수를 만들어 객체 생성을 담당하게 합니다.

fullName 은 이때 만들어지며, 실제 객체가 생성될 때, fullName 을 함께 넘겨줍니다. fullName 의 fileld(init=False) 를 삭제합니다.

추가. @dataclass(kw_only=True)

간편한 기능으로 kw_only 기능이 있습니다. 이 기능을 선언하면, 객체 생성시에 keyword를 꼭 입력해야합니다.

명시적으로 keyword 를 사용하게 하면서, 사용시 오류를 줄이도록 강제할 수 있습니다.

from dataclasses import dataclass, field

@dataclass(kw_only=True)
class Employee:
    firstName: str = field(repr=False)
    lastName: str = field(repr=False)

# employee = Employee("Yang","SeungSang") : Error
employee = Employee(firstName="Yang", lastName="SeungSang")

 

파이썬에서 정말 유용한 기능이라고 생각합니다. 잘 사용해보시기 바랍니다.

반응형
Comments