본문 바로가기

Computer/Python

파이썬 dataclasses 표준 라이브러리

반응형

파이썬 3.7 버젼은 3.x 버젼에서 유독 중요한 변경 사항이 많이 반영된 버젼입니다. 그 중에 특이할 점은 dataclasses 라는 표준 라이브러리가 소개되었다는 점인데요, 저도 최근에야 사용하기 시작했는데, 파이썬 클래스 작성 시 매우 효율적으로 코드를 구성할 수 있어 소개하고자 합니다. 파이썬의 클래스를 선언하려면 __init__ 메소드에 많은 아규먼트를 전달해주어야 하고 아규먼트가 줄거나 늘때마다 __init__ 메소드 안에서 인스턴스 멤버 변수로 선언해주는 코드 또한 매번 작성해야 했습니다. 따라서 __init__ 메소드에 전달해야 하는 아규먼트가 늘어날 수록 불필요한 코드의 양이 증가하게 됩니다. dataclasses 라이브러리를 사용하면 코드를 훨씬 컴팩트하게 작성할 수 있는데요, towarddatascience article을 기반으로 살펴보도록 하겠습니다.


예를 들어 다음과 같은 클래스가 있습니다. 이름, 나이, 직업 등의 아규먼트를 전달해주어야 하죠. 만약 한 사람에 대한 정보가 더 필요할 수록 아규먼트가 증가할 것이고 따라서 코드의 줄 또한 증가할 것입니다. 

class Person():
    def __init__(self, first_name, last_name, age, job):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
        self.job = job

하지만 dataclasses 라이브러리를 import 하고 class 위에 데코레이터 형태로 감싸주면 코드가 훨씬 깔끔해집니다. 밑 코드의 특징은 각 아규먼트를 한 번씩 type annotation 과 함께 한 번씩만 선언해주고 "self.xx=xx" 식의 인스턴스 멤버 변수를 생략해도 된다는 점입니다. 즉, dataclass로 해당 custom class를 감싸주면 각 아규먼트를 한 번씩만 선언만 해주어도 인스턴스 멤버 변수로 자동으로 할당된다는 것이죠. 심지어 inspect 라이브러리를 통해 Person 클래스의 메소드를 살펴보면 분명 아무 메소드도 직접적으로 정의한 적이 없지만 __init__, __eq__, __repr__ 메소드가 자동으로 정의된 것을 볼 수 있습니다. 

import inspect
from dataclasses import dataclass

@dataclass
class Person:
     first_name: str
     last_name: str
     age: int
     job: str

print(inspect.getmembers(Person, inspect.isfunction))

>>> 
[('__eq__', <function __create_fn__.<locals>.__eq__ at 0x7f863814ec20>), 
('__init__', <function __create_fn__.<locals>.__init__ at 0x7f8638144830>),
('__repr__', <function __create_fn__.<locals>.__repr__ at 0x7f8638144170>)]

또한, 당연하게도 클래스의 각 아규먼트 (혹은 field) 별로 디폴트 값을 선언해줄 수 있습니다. 이때 주의할 것은 디폴트 값이 선언되지 않은 필드가 디폴트 값이 선언된 필드보다 뒤에 위치할 수 없다는 것입니다. 디폴트 값이 선언되지 않은 필드에는 클래스의 인스턴스를 선언할 때 값을 넣어주어야 합니다. 

import inspect
from dataclasses import dataclass

@dataclass
class Person:
     first_name: str 
     last_name: str = 'A'
     age: int = 30
     job: str = 'JobLess'

a = Person('b')
print(a)

>>> Person(first_name='b', last_name='A', age=30, job='JobLess')
  • dataclass가 __repr__ 메소드도 자동으로 정의해주니 print 문으로 인스턴스를 출력했을 때 깔끔하게 출력되는 것을 볼 수 있습니다.

선언된 디폴트 값을 변하지 않게 하려면 dataclass의 frozen 아규먼트를 True로 바꾸어주면 됩니다. frozen=True로 설정되었을 때 인스턴스 멤버 변수를 바꾸려 시도하면 FrozenInstanceError가 발생합니다.

from dataclasses import dataclass

@dataclass(frozen=True)
class Person:
     first_name: str 
     last_name: str = 'A'
     age: int = 30
     job: str = 'JobLess'

a = Person('b')
a.last_name = 'hong'

>>>
---------------------------------------------------------------------------
FrozenInstanceError                       Traceback (most recent call last)
<ipython-input-7-01d804c2d0eb> in <module>()
     10 
     11 a = Person('b')
---> 12 a.last_name = 'hong'
     13 print(a)

<string> in __setattr__(self, name, value)

FrozenInstanceError: cannot assign to field 'last_name'

또한, 어떤 상황에서는 클래스가 생성될 때가 아닌 다른 field 값을 보고 값을 정해야 하는 field 들도 있습니다. 이와 같은 경우에는 dataclasses 라이브러리의 field 함수를 호출하고 field 함수의 init 아규먼트를 False로 두면 가능합니다. 하지만 인스턴스를 생성한 이후에 값을 따로 넣어주는 과정이 필요한데 이것은 __post_init__ 메소드를 불러와서 수행합니다. 밑의 예를 보면 __post_init__ 메소드는 __init__ 메소드가 불려진 뒤 실행되며 first_name, last_name 필드를 합쳐 full_name 이라는 필드의 값을 정의합니다. 즉, __post_init__ 메소드를 통해 인스턴스를 생성한 이후에 생성시 전달된 필드의 값들로부터 새로운 필드의 값을 만들어낼 수 있는 것이죠.

from dataclasses import dataclass, field

@dataclass
class Person:
     first_name: str = "Ahmed"
     last_name: str = "Besbes"
     age: int = 30
     job: str = "Data Scientist"
     full_name: str = field(init=False, repr=True)
     def __post_init__(self):
         self.full_name = self.first_name + " " + self.last_name

ahmed = Person()
print(ahmed)
ahmed.full_name

>>>
Person(first_name='Ahmed', last_name='Besbes', age=30, job='Data Scientist', full_name='Ahmed Besbes')
Ahmed Besbes

__eq__ 메소드는 클래스의 매직 메소드 중 하나로서 클래스 인스턴스들이 같은지 다른지 비교하는 메소드입니다. 일반적인 클래스에 대해서는 당연히 따로 구현되어 있어야하나 dataclass로 감싸는 순간 __eq__ 메소드가 정의되어 클래스 인스턴스 비교가 가능해집니다.

import inspect
from dataclasses import dataclass

@dataclass(frozen=True)
class Person:
     first_name: str 
     last_name: str = 'A'
     age: int = 30
     job: str = 'JobLess'

a = Person('b')
b = Person('b')
print(a==b)

>>> True

Equality 비교 (==) 이외에 크거나 작거나 등의 다른 타입의 비교를 수행하기 위해서는 __lt__ (less than), __le__ (less or equal), __gt__ (greater than), __ge__ (greater or equal) 등의 매직 메소드 구현이 필요합니다. dataclass 에서는 기본으로 __eq__ 메소드는 제공하고 order 아규먼트를 True로 설정하면 __eq__ 메소드 이외의 다른 비교 메소드도 제공합니다. 이때 어떤 필드의 값을 기준으로 비교를 할 것인지 정할 수 있는데, sort_index라는 필드를 정의하고 그 필드에 기준으로 삼을 필드를 대입하면 해당 필드를 중심으로 인스턴스들을 비교할 수 있습니다.

from dataclasses import dataclass, field

@dataclass(order=True)
class Person:
     first_name: str = "Ahmed"
     last_name: str = "Besbes"
     age: int = 30
     job: str = "Data Scientist"
     sort_index: int = field(init=False, repr=False)
     def __post_init__(self):
         self.sort_index = self.first_name

p1 = Person(first_name='aa')
p2 = Person(first_name='ab')

print(p1 > p2)

>>> False

마지막으로 다음처럼 tuple이나 dictionary 형태로의 변환도 astuple, asdict 함수를 이용하면 쉽게 변환이 가능합니다.

from dataclasses import dataclass, astuple, asdict

@dataclass
class Person:
    first_name: str = "Ahmed"
    last_name: str = "Besbes"
    age: int = 30
    job: str = "Data Scientist"

ahmed = Person()
print(astuple(ahmed))
>>> ('Ahmed', 'Besbes', 30, 'Data Scientist')
print(asdict(ahmed))
{'first_name': 'Ahmed', 'last_name': 'Besbes', 'age': 30, 'job': 'Data Scientist'}

 

참조

반응형