일반적으로 파이썬에서 클래스를 선언할 때는 __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)
클래스 내부에서 오버로딩해서 쓸 수 있는 특별한 매서드. 정확히는 던더 메서드(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
다. 간단히 클래스를 선언하는 것만으로 위에서 던더 메서드로 구현한 기능을 쓸 수 있게 해준다.
사용법은 간단하다.
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)]
이제 의도대로 됐다.
dataclass
를 사용하는 방법을 간단히 알아봤다. @dataclass
와 field
의 특성에는 이 글에서 소개하고 있는 것 말고도 많은 기능이 있고 공식문서에서 자세히 다루고 있다.