Sep.23.21

iissaacc·2021년 9월 23일
0

TIL

목록 보기
7/10

Prologue

일반적으로 파이썬에서 클래스를 선언할 때는 __init__ 을 포함해서 만들어서 변수에 저장하는 식으로 쓰게된다.

class Character:
    def __init__(self, name: str, classes: str, level: int) -> None:
        self.name = name
        self.classes = classes
        self.level = level
        
p1 = Character("Kim", "Warrior", 21)
p2 = Character("Lee", "Rogue", 32)
p3 = Character("Kim", "Warrior", 21)

print(p1)
print(f"{p1.__class__} {p1.name} {p1.classes} {p1.level}")
print(f"p1 == p3: {p1 == p3}    p1.name == p3.name: {p1.name == p3.name}")
print(p1.level < p2.level)
print(p1 < p2)
output.
<__main__.Character object at 0x000002BD31924F70>
<class '__main__.Character'> Kim Warrior 21
p1 == p3: False    p1.name == p3.name: True
False
Traceback (most recent call last):
  File "no_dataclass.py", line 16, in <module>
    print(p1 > p2)
TypeError: '>' not supported between instances of 'Character' and 'Character'

인스턴스를 출력해서는 클래스와 메모리 주소만 알 수 있을 뿐 데이터를 들여다볼 수 없다. 그래서 서로 같은지 물어보면 메모리 주소로 인해 다르다고 할 뿐만 아니라 클래스끼리의 크기 비교는 더더욱 안 된다고 그런다. 굳이 하려면 p1.name이런 식으로 접근해야 한다. 직접 접근해서 데이터를 다룰 수도 있지만 오류를 낼 확률이 높아질 뿐만 아니라 데이터만 가지는 클래스를 굳이 만들어 쓸 이유는 없는 것 같다.

Magic method

매직메서드를 사용해서 클래스가 가진 기능을 더해주면 인스턴스가 가진 데이터를 들여다보거나 비교할 수 있다.

매직 메서드(magic method)
클래스 내부에서 오버로딩해서 쓸 수 있는 특별한 매서드. 정확히는 던더 메서드(dunder method)라고 부른다. 흔히 볼 수 있는 __init__이나 __call__처럼 명령어 앞뒤로 언더스코어를 두개씩 붙여서 쓴다. 종류는 이 글에서 볼 수 있는 것 외에도 4가지 기본 연산자 __add__, __sub__, __mul__, __div__, 데이터 타입을 정하는 __str__, __int__, __float__이 있다. 이 밖에도 생각할 수 있는 거의 모든 기능을 던더 메서드로 제공하고 있다. 필요한 기능이 있나 없나 찾아보도록 하자.

던더 메서드를 사용해서 클래스를 다시 만들어보자.

class Character:
    def __init__(self, name: str, classes: str, level: int) -> None:
        self.name = name
        self.classes = classes
        self.level = level

    def __repr__(self):
        return f"{__class__.__name__} {self.name} {self.classes} {self.level}"
        
    def __eq__(self, other):
        return self.name == other.name and self.classes == other.classes and self.level == other.level

    def __gt__(self, other):
        return self.level > other.level

p1 = Character("Kim", "Warrior", 21)
p2 = Character("Lee", "Rogue", 32)
p3 = Character("Kim", "Warrior", 21)

print(p1)
print(p1 == p3)
print(p1 < p2)
output
Character Kim Warrior 13
True
True

이러면 의도한대로 변수를 통해 인스턴스의 클래스를 확인하고 데이터를 확인할 수 있게 됐다.

@dataclass

이렇게 해도 일거리가 상당히 줄었다고 생각했는데 더 줄여주는 모듈이 있다. dataclass다. 간단히 클래스를 선언하는 것만으로 위에서 던더 메서드로 구현한 기능을 쓸 수 있게 해준다.

사용법은 간단하다.

from dataclasses import dataclass

@dataclass
class Character:
    name: str
    classes: str
    level: int

p1 = Character("Kim", "Warrior", 21)
p2 = Character("Kim", "Warrior", 21)

print(p1)
print(p1 == p2)
output
Character(name='Kim', classes='Warrior', level=13)
True

보는 것처럼 클래스변수만 들어있는 클래스에다가 @dataclass 데코레이터만 붙여서 쓴다. 그러면 데코레이터가 클래스 내부에 선언한 field를 살펴서 __init__, __repr__, __eq__를 자동으로 만든다.

field
타입힌트를 동반한 클래스변수. 필요하면 따로 field를 만들고 클래스 변수에 저장해서 필드의 성질을 바꿔줄 수도 있다.

이렇게만 했는데도 우리는 변수를 호출하는 것만으로 인스턴스의 데이터를 들여다보고 같은지 아닌지 판별할 수 있게 됐다. 아직은 대소관계를 비교할 수 없는데 기능추가는 일일이 코딩하지 않고 데코레이터 부분만조금 수정하면 되는 수준으로 간단하게 만들어 준다.

from dataclasses import dataclass

@dataclass(order=True)
class Character:
    name: str
    classes: str
    level: int
    
p1 = Character("K", "Warrior", 15)
p2 = Character("L", "Rogue", 9)
print(p2)
print(p1 < p2)
output
Character(name='L', classes='Rogue', level=9)
False

특히 order=True에 의해 클래스 내부에 자동적으로 __lt__, __le__, __gt__, __ge__가 만들어진다. 하나도 아니고 네 개씩이나! 넷 중 하나라도 클래스 내부에 만들었으면 타입에러가 난다. 이렇게 대소비교를 할 수 있으면 정렬도 할 수 있지 않을까? 그러면 level을 기준으로 정렬해보자.

p1 = Character("Kim", "Warrior", 15)
p2 = Character("Lee", "Rogue", 9)
mylist = [p1, p2]
mylist.sort()
print(mylist)
output
[Character(name='Kim', classes='Warrior', level=15), Character(name='Lee', classes='Rogue', level=9)]

??? sort는 분명 오름차순으로 정렬해주는데 안 되는 것 같지만 가장 먼저 나오는 name을 기준으로 정렬했다. 그러면 정렬기준을 바꿔보자. 정렬기준과 상관없는 field를 기준에서 빼는 것으로 성질을 바꿔달라고 해보자.

from dataclasses import dataclass, field

@dataclass(order=True)
class Character:
    name: str = field(compare=False)
    classes: str = field(compare=False)
    level: int
    
p1 = Character("Kim", "Warrior", 15)
p2 = Character("Lee", "Rogue", 9)
mylist = [p1, p2]
mylist.sort()
print(mylist)
output
[Character(name='L', classes='Rogue', level=9), Character(name='K', classes='Warrior', level=15)]

이제 의도대로 됐다.

Epilogue

dataclass를 사용하는 방법을 간단히 알아봤다. @dataclassfield의 특성에는 이 글에서 소개하고 있는 것 말고도 많은 기능이 있고 공식문서에서 자세히 다루고 있다.

Reference

https://docs.python.org/3/library/dataclasses.html

0개의 댓글