본문 바로가기
TYLER SONGBlog
블로그 목록
LLM Research

Python Class와 dataclass: 데이터와 동작을 하나로 묶는 법

지금까지 변수와 함수를 따로따로 다뤘다면, 오늘은 관련된 데이터와 그 데이터를 다루는 함수를 한 덩어리로 묶는 법을 배운다. 클래스(class)라는 틀로 벡터 계산기를 직접 만들어보고, 나중에 학습 설정값을 깔끔하게 관리할 때 쓰는 dataclass도 함께 익힌다. 이 둘은 앞으로 PyTorch 모델 코드나 실험 설정 파일을 읽고 쓸 때 계속 마주치게 된다

송민성8분 읽기

오늘의 목표

숫자 몇 개와 그 숫자를 다루는 함수들을 따로 흩어놓지 않고, "이 데이터는 이런 동작을 할 수 있다"는 하나의 묶음(클래스)으로 표현할 수 있게 되는 것이 오늘의 목표다. 예를 들어 "2차원 벡터"라는 개념을 코드로 만들면, 벡터끼리 더하거나 크기를 구하는 계산을 벡터 자체가 "할 줄 아는 일"처럼 다룰 수 있다.

개념과 직관

클래스(class)와 객체(object/instance)

지금까지는 이런 식으로 코드를 짰을 것이다.

python
x1, y1 = 1, 2 x2, y2 = 3, 4 sum_x = x1 + x2 sum_y = y1 + y2

벡터가 여러 개 있으면 변수 이름을 계속 늘려야 하고, "이 x1, y1은 하나의 벡터를 이룬다"는 관계가 코드에 드러나지 않는다.

클래스(class, 어떤 종류의 물건을 만들기 위한 설계도)는 "이런 데이터(속성)와 이런 동작(메서드)을 가진 물건을 만들겠다"는 틀이다. 그 틀로 실제 찍어낸 결과물이 객체(object) 또는 인스턴스(instance)다.

비유: 클래스는 붕어빵 틀, 객체는 그 틀로 구워낸 붕어빵 하나하나다. 틀은 하나지만 붕어빵은 여러 개 만들 수 있고, 각 붕어빵마다 안에 든 팥의 양(속성값)이 다를 수 있다.

__init__self

python
class Vector2D: def __init__(self, x, y): self.x = x self.y = y

__init__은 객체가 "생성될 때" 자동으로 실행되는 특별한 메서드(초기화 메서드, initializer)다. Vector2D(1, 2)라고 쓰면 파이썬이 알아서 __init__(새로_만든_객체, 1, 2)를 호출한다.

self는 "지금 만들어지고 있는(또는 다뤄지고 있는) 그 객체 자신"을 가리키는 이름이다. 이름은 관례상 self를 쓰지만 사실 아무 이름이나 가능하다. self.x = x는 "이 객체의 x라는 서랍에 매개변수로 받은 x값을 넣는다"는 뜻이다.

메서드(method)는 클래스 안에 정의된 함수인데, 항상 첫 번째 인자로 self를 받아서 "어떤 객체에 대해" 동작하는지 안다.

python
class Vector2D: def __init__(self, x, y): self.x = x self.y = y def add(self, other): return Vector2D(self.x + other.x, self.y + other.y)

v1.add(v2)라고 부르면 self 자리에 자동으로 v1이 들어간다.

인스턴스 속성(instance attribute)

self.x, self.y처럼 객체마다 따로 저장되는 값을 인스턴스 속성이라고 한다. 붕어빵마다 팥의 양이 다르듯, 같은 클래스로 만든 객체라도 속성값은 서로 다를 수 있다.

상속(inheritance)과 오버라이드(override)

상속은 "기존 클래스의 기능을 물려받아서, 일부만 바꾸거나 추가하는 것"이다.

비유: "동물"이라는 큰 틀이 있고, "강아지"는 동물의 특징(먹는다, 잔다)을 그대로 물려받으면서 "짖는다"는 동작만 추가하는 것과 같다.

python
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name}가 소리를 낸다" class Dog(Animal): # Animal을 상속 def speak(self): # 오버라이드: 부모의 메서드를 덮어씀 return f"{self.name}가 짖는다"

DogAnimal을 괄호 안에 써서 상속받는다. speak를 다시 정의하면 부모의 것 대신 자식의 것이 쓰이는데, 이를 오버라이드(override, 덮어쓰기)라고 한다. __init__을 새로 안 썼으니 DogAnimal__init__을 그대로 쓴다.

__repr__

python
class Vector2D: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Vector2D({self.x}, {self.y})"

__repr__이 없으면 print(v1)을 했을 때 <__main__.Vector2D object at 0x7f...> 같은 알아보기 힘든 주소값이 나온다. __repr__을 정의하면 객체를 사람이 읽기 좋은 문자열로 표현할 수 있다. 디버깅할 때 값이 눈에 바로 보이므로 실무에서 거의 항상 정의한다.

@dataclass

클래스를 만들다 보면 "값을 저장만 하는" 단순한 클래스를 자주 쓰게 된다. 이럴 때 __init__, __repr__을 매번 손으로 쓰는 게 번거로운데, dataclass가 이걸 자동으로 만들어준다.

python
from dataclasses import dataclass @dataclass class Config: learning_rate: float batch_size: int epochs: int = 10 # 기본값 지정 가능

@dataclass는 데코레이터(decorator, 함수나 클래스에 추가 기능을 씌워주는 문법)다. 이걸 클래스 위에 붙이면 __init____repr__을 자동으로 만들어준다. Config(learning_rate=0.001, batch_size=32)라고 쓰면 나머지 epochs는 기본값 10이 들어간다.

코드로 직접 해보기

1단계: 2D 벡터 클래스 만들기

python
import math class Vector2D: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Vector2D({self.x}, {self.y})" def add(self, other): return Vector2D(self.x + other.x, self.y + other.y) def dot(self, other): # 내적: 각 성분끼리 곱한 뒤 더한다 return self.x * other.x + self.y * other.y def norm(self): # 크기(길이): 피타고라스 정리처럼 제곱합의 제곱근 return math.sqrt(self.x ** 2 + self.y ** 2) v1 = Vector2D(3, 4) v2 = Vector2D(1, 2) print(v1.add(v2)) # Vector2D(4, 6) print(v1.dot(v2)) # 11 (3*1 + 4*2) print(v1.norm()) # 5.0 (3-4-5 직각삼각형)

내적(dot product)의 수식은 텍스트로 쓰면: dot = x1*x2 + y1*y2. 크기(norm, 벡터의 길이)는 norm = sqrt(x^2 + y^2)다. 이건 나중에 "두 벡터가 얼마나 비슷한 방향인지", "벡터가 얼마나 큰지"를 잴 때 그대로 다시 나오는 계산이다.

2단계: 연산자 오버로딩으로 자연스럽게 쓰기 (선택)

v1.add(v2) 대신 v1 + v2처럼 쓰고 싶다면 __add__라는 특수 메서드를 추가하면 된다.

python
class Vector2D: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Vector2D({self.x}, {self.y})" def __add__(self, other): return Vector2D(self.x + other.x, self.y + other.y) v1 = Vector2D(3, 4) v2 = Vector2D(1, 2) print(v1 + v2) # Vector2D(4, 6)

3단계: 상속으로 확장하기

python
class Vector3D(Vector2D): def __init__(self, x, y, z): super().__init__(x, y) # 부모의 __init__을 그대로 활용 self.z = z def __repr__(self): return f"Vector3D({self.x}, {self.y}, {self.z})" def norm(self): # 오버라이드: 3차원용으로 재정의 return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) v3 = Vector3D(1, 2, 2) print(v3) # Vector3D(1, 2, 2) print(v3.norm()) # 3.0

super()는 "부모 클래스"를 가리키는 키워드다. super().__init__(x, y)는 "부모의 초기화 로직을 그대로 실행해줘"라는 뜻이라서, x와 y를 저장하는 코드를 다시 쓸 필요가 없다.

4단계: dataclass로 Config 만들기

python
from dataclasses import dataclass @dataclass class Config: learning_rate: float batch_size: int epochs: int = 10 model_name: str = "toy-model" cfg = Config(learning_rate=0.001, batch_size=32) print(cfg) # Config(learning_rate=0.001, batch_size=32, epochs=10, model_name='toy-model') cfg2 = Config(learning_rate=0.0005, batch_size=64, epochs=20) print(cfg2.epochs) # 20

__init__도, __repr__도 안 썼는데 자동으로 만들어진 걸 확인할 수 있다.

오늘의 실험

오늘은 별도 실험 없음. 대신 위 코드를 그대로 따라 친 뒤, 다음 두 가지를 직접 눈으로 확인해보자.

  1. __repr__을 지운 Vector2D를 만들어 print(v1)을 해보고, 주소값이 찍히는 걸 확인한다.
  2. Config 클래스에서 @dataclass를 잠깐 지워보고 Config(0.001, 32)를 실행하면 무슨 에러가 나는지 읽어본다. 에러 메시지를 읽는 연습 자체가 실험이다.

왜 이걸 배우나 (LLM / Post-training 연결)

PyTorch로 신경망을 만들 때 모델 자체가 클래스로 정의된다(class MyModel(nn.Module)처럼 상속을 쓴다). 오늘 배운 __init__, self, 상속, 오버라이드 구조를 그대로 가져다 쓰게 될 것이다. 또한 실험 설정(학습률, 배치 크기, 에폭 수 등)을 관리할 때 dataclass나 이와 비슷한 방식(config 클래스)을 실무에서 거의 항상 사용한다. Post-training 실험을 여러 개 돌릴 때 "이 실험은 어떤 설정으로 돌렸는지"를 dataclass 하나로 명확히 기록해두는 습관이 여기서 시작된다.

초보자가 막히는 지점

  • self를 깜빡하고 메서드 정의에서 빼먹는 실수가 가장 흔하다. def add(other):처럼 쓰면 TypeError: add() takes 1 positional argument but 2 were given 같은 에러가 난다. 메서드는 항상 첫 인자로 self를 받는다는 걸 기억하자.
  • self.x = x에서 왼쪽 self.x와 오른쪽 x가 이름이 같아서 헷갈리기 쉽다. 왼쪽은 "이 객체 안에 저장할 서랍 이름", 오른쪽은 "매개변수로 받은 값"으로, 둘은 별개다.
  • 클래스 정의(class Vector2D:)와 인스턴스 생성(v1 = Vector2D(3, 4))을 혼동하지 말 것. 클래스는 설계도이고, Vector2D(3, 4)처럼 괄호를 붙여 호출해야 실제 객체가 만들어진다.
  • 상속에서 __init__을 새로 정의하면서 super().__init__()을 빼먹으면 부모가 설정해야 할 속성이 아예 없어져서, 나중에 부모의 메서드를 호출할 때 AttributeError가 난다.
  • @dataclassimport를 잊으면 당연히 작동하지 않는다. from dataclasses import dataclass를 꼭 파일 맨 위에 써야 한다.

알고리즘 (병행 1시간)

오늘은 자료구조 관련 문제 두 개를 푼다.

  • LeetCode Easy 1개: 자료구조(리스트, 딕셔너리, 스택 등) 기본기를 다지는 문제로 아무거나 하나 선택 (예: 유효한 괄호, 두 수의 합 같은 유형)
  • LeetCode Medium 1개: 조금 더 복잡한 자료구조 조작이 필요한 문제 하나

풀이 후 기록할 것:

  • 접근 방식: 어떤 자료구조(딕셔너리, 스택, 리스트 등)를 왜 선택했는지 한두 줄로 적기
  • 시간복잡도: Big-O로 대략 어느 정도인지 (아직 정확히 몰라도 "반복문이 몇 겹인지"로 추측해보기)
  • 틀린 이유: 처음에 틀렸다면 어떤 케이스를 놓쳤는지, 어떤 착각을 했는지 구체적으로 한 줄 남기기 (예: "빈 리스트 처리를 안 함", "인덱스가 1개 밀려서 계산됨")

이 기록은 나중에 비슷한 실수를 반복하지 않기 위한 자산이 되니, 짧더라도 꼭 남겨두자.

© 2026 Tyler Song