Skip to content
This page has been auto-translated and may contain errors.View in English

클래스와 객체

지금까지 다뤄온 모든 타입(문자열, 리스트, 딕셔너리)은 사실 클래스입니다. "hello".upper()를 호출하면 문자열 객체의 메서드를 호출하는 것입니다. 클래스를 사용하면 자신만의 데이터와 동작을 가진 자신만의 타입을 정의할 수 있습니다. Player 클래스는 이름, 점수, 레벨을 가지고 있고 스스로를 표시하는 방법도 알 수 있습니다.

클래스는 사용자 정의 타입을 위한 메커니즘입니다. 클래스는 템플릿을 정의합니다: 각 인스턴스가 보유하는 데이터(속성)와 지원하는 연산(메서드)입니다. 값들을 병렬 변수로 추적하고 여기저기 전달하는 대신, 명확한 인터페이스를 가진 하나의 응집된 객체로 묶을 수 있습니다.

class 문은 메타클래스(기본값 type)를 사용하여 현재 네임스페이스에 새로운 타입 객체를 생성합니다. 인스턴스는 클래스를 호출함으로써 생성되며, 이는 객체를 할당하는 __new__와 초기화하는 __init__을 호출합니다. Python의 객체 모델은 균일합니다: 모든 것이 객체이며, 클래스, 함수, 그리고 타입 자체도 마찬가지입니다.

청사진과 인스턴스

클래스는 청사진입니다. 인스턴스는 그 청사진으로부터 만들어진 구체적인 것입니다. 필요한 만큼 많은 인스턴스를 만들 수 있으며, 각각은 자신의 데이터를 가지지만 클래스에 정의된 동일한 메서드를 공유합니다.

클래스는 구조와 동작을 정의합니다. 인스턴스는 그 클래스로부터 생성된 객체이며, 각각 자신만의 속성 네임스페이스를 가지지만 클래스의 메서드 객체는 공유합니다. 인스턴스를 생성하려면 함수처럼 클래스를 호출합니다: Dog()는 새로운 Dog 인스턴스를 생성합니다.

클래스를 호출하면 type.__call__이 호출되며, 이는 인스턴스를 할당하기 위해 cls.__new__(cls)를 호출하고 초기화를 위해 cls.__init__(instance, ...)를 호출합니다. 결과 객체의 __class__는 다시 클래스를 가리킵니다. 메서드 조회는 MRO를 따릅니다: 인스턴스 __dict__, 클래스 __dict__, 그리고 MRO 체인 순서입니다.

python
class Dog:
    def bark(self):
        print("Woof!")

rex  = Dog()
luna = Dog()

rex.bark()    # "Woof!"
luna.bark()   # "Woof!"

Dog는 클래스입니다. rexluna는 인스턴스입니다: 두 마리의 다른 강아지이며, 각각 클래스에 정의된 동일한 동작을 공유합니다.

__init__self

__init__은 새로운 인스턴스를 생성할 때 Python이 자동으로 호출하는 메서드입니다. 객체의 초기 데이터를 설정하는 곳입니다. self는 메서드가 작동 중인 특정 인스턴스를 참조하는 방법이며, 항상 첫 번째 매개변수입니다.

__init__은 갓 할당된 인스턴스를 초기화합니다. self는 모든 인스턴스 메서드의 첫 번째 매개변수에 대한 관례적인 이름이며, alice.display()를 호출할 때 Python이 인스턴스를 자동으로 전달합니다. __init__ 내부에서 self에 설정된 속성은 인스턴스 속성입니다: 각 인스턴스는 자신만의 복사본을 가집니다.

__init____new__로부터 이미 할당된 인스턴스를 받아 설정합니다. self는 키워드가 아닙니다; 첫 번째 위치 매개변수이며, instance.method()를 호출할 때 디스크립터 프로토콜에 의해 암묵적으로 전달됩니다. self.attr = value를 설정하면 object.__setattr__이 호출되며, 이는 인스턴스의 __dict__에 기록합니다. __init__에서 설정되지 않은 속성도 나중에 어떤 인스턴스에서든 설정될 수 있는데, Python 딕셔너리는 동적이기 때문입니다.

python
class Player:
    def __init__(self, name, score=0):
        self.name  = name
        self.score = score

    def add_points(self, points):
        self.score += points

    def display(self):
        print(f"{self.name}: {self.score} points")

alice = Player("민준")
bob   = Player("서연", score=50)

alice.add_points(30)
alice.display()   # "민준: 30 points"
bob.display()     # "서연: 50 points"

self.nameself.score인스턴스 속성입니다: 클래스 자체가 아니라 특정 객체에 속합니다. 각 Player 인스턴스는 자신만의 namescore를 가집니다.

메서드

클래스 내부에 정의된 모든 함수는 메서드입니다. 인스턴스 메서드는 항상 self를 첫 번째 매개변수로 가지며, Python이 자동으로 전달합니다. 메서드는 self를 통해 인스턴스의 데이터를 읽고 변경할 수 있습니다.

인스턴스 메서드는 클래스 네임스페이스에 저장된 일반 함수입니다. 디스크립터 프로토콜은 instance.method바운드 메서드로 변환하며, self를 자동으로 바인딩합니다. 메서드에서 self를 반환하면 메서드 체이닝이 가능해집니다: obj.scale(2).rotate(90).

instance.method는 디스크립터 조회를 트리거합니다: type.__getattribute__는 클래스 __dict__에서 method를 찾고, 이를 함수(디스크립터 프로토콜을 구현)로 인식한 다음, 인스턴스가 미리 적용된 함수를 감싸는 바운드 메서드 객체를 반환합니다. 이것이 self가 자동으로 전달되는 방식입니다. self를 반환하면 플루언트 인터페이스가 가능합니다.

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def scale(self, factor):
        self.radius *= factor
        return self    # self를 반환하면 체이닝이 가능합니다: c.scale(2).scale(0.5)

c = Circle(5)
print(c.area())    # 78.53975
c.scale(2)
print(c.area())    # 314.159

클래스 변수 vs 인스턴스 변수

클래스에 직접 정의된 변수(__init__ 내부가 아닌)는 클래스 변수입니다. 모든 인스턴스는 동일한 클래스 변수를 공유합니다. __init__ 내부에서 self에 설정된 변수는 인스턴스 변수이며, 각 객체에 고유합니다.

클래스 변수는 클래스 __dict__에 존재하며 모든 인스턴스에 의해 공유됩니다. 인스턴스 변수는 각 인스턴스의 __dict__에 존재합니다. self.attr을 읽을 때 Python은 인스턴스를 먼저 확인한 다음 클래스를 확인합니다. self.attr = value를 작성하면 항상 인스턴스 속성을 생성하거나 업데이트하며, 만약 클래스 변수가 존재하면 그것을 가립니다.

인스턴스에서의 속성 조회는 다음 순서를 따릅니다: 인스턴스 __dict__, 클래스 __dict__, 그리고 MRO. self.attr에 쓰는 것은 항상 인스턴스 __dict__를 대상으로 합니다(__setattr__이 오버라이드되지 않은 경우). 클래스 변수는 self.attr에 쓰기가 인스턴스 섀도우를 만들기 전까지 공유됩니다. 가변 클래스 변수(예: 리스트)는 특히 까다롭습니다: 모든 인스턴스가 동일한 객체를 공유하므로, 어떤 인스턴스를 통한 변경도 모두에게 보입니다.

python
class Player:
    max_lives = 3    # 클래스 변수, 모든 Player에 대해 동일

    def __init__(self, name):
        self.name  = name   # 인스턴스 변수, 각 Player에 고유
        self.lives = Player.max_lives

    def die(self):
        self.lives -= 1

alice = Player("민준")
bob   = Player("서연")

Player.max_lives = 5    # 현재 및 미래의 모든 인스턴스에 대해 변경

모든 인스턴스에서 공유되는 값(상수, 카운터, 기본값)에는 클래스 변수를 사용하세요. 객체마다 다른 데이터에는 인스턴스 변수를 사용하세요.

__str____repr__

__str__print()와 f-string이 객체에 대해 보여주는 내용을 제어합니다. __repr__은 콘솔에 표시되고 디버깅에 사용되는 개발자용 뷰를 제어합니다. 항상 __repr__을 정의하세요. 디버그 뷰와 별도로 깔끔한 사용자 대상 표시를 원할 때 __str__을 정의하세요.

__str__str(), print(), 그리고 f-string에 의해 호출됩니다. __repr__repr()에 의해 호출되며 REPL에 표시됩니다. __repr__만 정의되어 있으면 Python은 둘 다에 그것을 사용합니다. 관례는 다음과 같습니다: __repr__은 객체를 재구성할 수 있는 문자열을 반환해야 하고, __str__은 사람이 읽을 수 있는 요약을 반환해야 합니다.

str(obj)type(obj).__str__(obj)를 호출하며, __str__이 정의되어 있지 않으면 __repr__로 대체됩니다. repr(obj)type(obj).__repr__(obj)를 호출합니다. f-string 내부에서 {obj}__format__을 호출하며, 기본적으로 __str__을 호출합니다. {obj!r}은 포맷팅 전에 repr()을 적용합니다. __repr__ 관례: 평가될 때 객체를 재현할 수 있는 ClassName(arg1=..., arg2=...) 형식의 문자열을 반환합니다.

python
class Player:
    def __init__(self, name, score):
        self.name  = name
        self.score = score

    def __str__(self):
        return f"{self.name} ({self.score} pts)"

    def __repr__(self):
        return f"Player(name={self.name!r}, score={self.score})"

alice = Player("민준", 87)
print(alice)        # "민준 (87 pts)"   (__str__ 사용)
repr(alice)         # "Player(name='민준', score=87)"  (__repr__ 사용)

항상 __repr__을 정의하세요. 디버그 뷰와 별도로 깔끔한 사용자 대상 표현을 원할 때 __str__을 정의하세요. __repr__만 정의되어 있으면 Python은 둘 다에 그것을 사용합니다.

비공개 관례

Python에는 진정으로 비공개인 변수는 없지만, 이름 시작 부분의 단일 밑줄(_balance)은 "이것은 내부용이며, 클래스 외부에서 직접 사용하지 마세요"를 알리는 관례입니다. 언어에 의해 강제되지 않으며, 다른 개발자에게 보내는 의사소통입니다.

단일 밑줄(_attr)은 내부 사용을 알리는 관례입니다. Python은 이를 강제하지 않지만, 모든 린터, IDE, 개발자가 이를 존중합니다. 이중 밑줄(__attr)은 이름 맹글링을 트리거합니다: Python은 이를 _ClassName__attr로 다시 작성하여 서브클래스에서의 우발적 충돌을 방지합니다. 진정한 비공개성이 아니라 충돌 방지 메커니즘입니다.

단일 밑줄: 관례일 뿐, 강제는 없습니다. 이중 밑줄: 이름 맹글링은 컴파일 시점에 __attr_ClassName__attr로 변환하며, 런타임이 아닙니다. 이는 서브클래스 충돌 방지이지 비공개 메커니즘이 아닙니다; 맹글링된 이름은 여전히 외부에서 접근 가능합니다. __slots__은 더 근본적인 제어입니다: 인스턴스 __dict__를 고정된 슬롯 디스크립터 집합으로 대체하여 임의의 속성 생성을 방지하고 메모리 오버헤드를 줄입니다.

python
class BankAccount:
    def __init__(self, balance):
        self._balance = balance    # _는 "건드리지 마세요"를 의미합니다

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def balance(self):
        return self._balance

이중 밑줄(__name)은 이름 맹글링을 트리거합니다; Python은 서브클래스에서의 충돌을 피하기 위해 속성을 _ClassName__name으로 이름을 변경합니다. 거의 필요하지 않습니다. 단일 밑줄이 대부분의 코드에서 사용되는 관례입니다.

상속

클래스는 다른 클래스로부터 상속받을 수 있으며, 그 클래스의 모든 속성과 메서드를 자동으로 얻습니다. 그런 다음 서브클래스에서 특정 메서드를 오버라이드하여 동작을 변경할 수 있습니다. 이를 통해 공통 기반을 재사용하고 필요한 곳에서 특수화할 수 있습니다.

상속은 "is-a" 관계를 만듭니다. 서브클래스는 부모의 모든 메서드와 속성을 상속받으며, 그 중 어느 것이든 오버라이드할 수 있습니다. 메서드 결정 순서(MRO)는 속성에 대한 조회 순서를 정의합니다. Python은 다중 상속을 지원합니다; MRO는 C3 선형화 알고리즘으로 계산됩니다.

MRO는 C3 선형화를 사용하는 type.__mro_entries__에 의해 계산되며, ClassName.__mro__로 접근 가능합니다. 메서드 결정은 MRO를 왼쪽에서 오른쪽으로 따릅니다. isinstance(obj, cls)는 확인을 위해 MRO를 순회합니다. 다중 상속은 강력하지만 다이아몬드 상속을 만들 수 있습니다; MRO는 이를 결정론적으로 해결합니다. super()는 직접 부모 클래스가 아닌 MRO를 따릅니다.

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
    print(pet.speak())

DogCatAnimal로부터 __init__을 상속받으므로 자신만의 것이 필요하지 않습니다. 자신만의 특정 동작으로 speak()를 오버라이드합니다.

super()

super()는 부모 클래스의 메서드를 호출합니다. 부모의 동작을 완전히 대체하기보다는 확장하고 싶을 때 사용하세요: 부모의 설정을 실행하기 위해 부모의 __init__을 호출한 다음, 서브클래스에 필요한 것을 그 위에 추가합니다.

super()는 MRO에서 다음 클래스로 메서드 호출을 위임하는 프록시 객체를 반환합니다. 부모에게 __init__이 있을 때는 항상 서브클래스 __init__에서 super().__init__()을 호출하세요. 이를 건너뛰면 부모의 설정 코드가 실행되지 않아 객체가 깨진 상태로 남을 수 있습니다.

인자 없는 super()(Python 3)는 MRO 위치를 결정하기 위해 __class__(컴파일러가 주입한 셀 변수)와 둘러싼 메서드의 첫 번째 인자를 사용합니다. super(cls, self)는 명시적인 형태입니다. 프록시는 cls 이후의 type(self).__mro__의 다음 클래스부터 시작하여 메서드 조회를 해결합니다. 이는 올바른 다중 상속에 필수적입니다: super()는 전체 MRO를 통해 각 클래스의 __init__을 체이닝합니다.

python
class Animal:
    def __init__(self, name, sound):
        self.name  = name
        self.sound = sound

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")   # Animal.__init__ 호출
        self.tricks = []                  # 추가 기능 더하기

    def learn(self, trick):
        self.tricks.append(trick)

rex = Dog("Rex")
rex.learn("sit")
print(rex.tricks)   # ["sit"]

서브클래스가 자신만의 __init__을 가지고 있고 부모도 가지고 있을 때는 항상 super().__init__()을 호출하세요.

클래스 메서드와 정적 메서드

@classmethod는 인스턴스 대신 클래스 자체를 받는 메서드를 만듭니다. 대체 생성자에 유용합니다: 문자열, 파일, 또는 다른 형식으로부터 인스턴스를 생성하는 경우입니다. @staticmethod는 조직화를 위해 클래스 안에 있는 평범한 함수입니다; 인스턴스도 클래스도 받지 않습니다.

@classmethod는 인스턴스가 아닌 cls(클래스)를 첫 번째 인자로 받습니다. 주요 용도는 다양한 입력 형식으로부터 인스턴스를 생성하는 대체 생성자입니다. @staticmethod는 클래스 아래에 네임스페이싱된 일반 함수입니다; 클래스나 인스턴스에 접근할 수 없습니다. 생성자에는 @classmethod를, 클래스와 논리적으로 연관된 유틸리티 함수에는 @staticmethod를 사용하세요.

@classmethod는 디스크립터 프로토콜을 사용합니다: classmethod 디스크립터는 첫 번째 인자가 인스턴스가 아닌 클래스인 바운드 메서드를 반환합니다. cls는 호출 시점의 실제 클래스이므로 서브클래스 메서드는 서브클래스를 받습니다. @staticmethod는 더 간단한 디스크립터로, 암묵적인 첫 번째 인자 없이 기본 함수를 변경하지 않고 반환합니다. 두 데코레이터 모두 디스크립터 동작을 수정하지만, 함수의 코드 객체에는 영향을 주지 않습니다.

python
class Player:
    def __init__(self, name, score):
        self.name  = name
        self.score = score

    @classmethod
    def from_string(cls, data):
        name, score = data.split(",")
        return cls(name, int(score))

alice = Player.from_string("민준,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("민준")   # True
Player.is_valid_name("A1")      # False

대체 생성자에는 @classmethod를 사용하세요. 클래스와 논리적으로 연관되지만 인스턴스나 클래스 데이터가 필요하지 않은 유틸리티 함수에는 @staticmethod를 사용하세요.

@property

@property를 사용하면 괄호 없이 속성처럼 메서드에 접근할 수 있습니다. 다른 속성으로부터 계산되어 단순 속성 접근으로 읽는 것이 자연스러운 값에 사용하세요.

@property는 메서드를 읽기 전용 속성으로 변환합니다. 속성에 접근할 때 메서드가 실행됩니다. 이는 저장된 데이터로부터 파생된 계산 값에 유용하며, 공개 인터페이스를 변경하지 않고 속성 접근에 유효성 검사를 추가하는 데에도 유용합니다. 짝을 이루는 @name.setter는 속성을 쓰기 가능하게 만듭니다.

@property는 디스크립터입니다: property(fget, fset, fdel, doc). instance.attr에 접근하면 fget(instance)가 호출됩니다. @name.setterfset을 설정합니다. @name.deleterfdel을 설정합니다. 프로퍼티는 인스턴스가 아닌 클래스에서 조회되며, 이것이 instance.__dict__[name]이 프로퍼티를 가리지 못하는 이유입니다(데이터 디스크립터가 인스턴스 __dict__보다 우선합니다). 프로퍼티는 Java 스타일의 게터/세터 메서드에 대한 Python다운 대안입니다.

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)      # 78.53975 (속성처럼 보이지만 메서드처럼 실행됨)
print(c.diameter)  # 10

프로퍼티는 계산 값에 유용합니다: () 없이 접근하는 것이 자연스럽게 느껴지는 다른 속성으로부터 파생된 것들입니다.

실전에서

인스턴스 속성, 메서드, @property, 그리고 __str__을 가진 Player 클래스:

python
class Player:
    max_lives = 3

    def __init__(self, name: str):
        self.name  = name
        self.score = 0
        self.lives = Player.max_lives

    def earn_points(self, amount: int) -> None:
        self.score += amount

    def take_hit(self) -> bool:
        self.lives -= 1
        return self.lives > 0

    @property
    def is_alive(self) -> bool:
        return self.lives > 0

    def __str__(self) -> str:
        return f"{self.name} | Score: {self.score} | Lives: {self.lives}"

alice = Player("민준")
alice.earn_points(50)
alice.take_hit()
print(alice)            # "민준 | Score: 50 | Lives: 2"
print(alice.is_alive)   # True

@property 게터를 가진 비공개 속성, deactivate 메서드, 그리고 to_dict 직렬화 메서드를 사용하는 User 클래스:

python
class User:
    def __init__(self, user_id: int, username: str, email: str):
        self.id       = user_id
        self.username = username
        self.email    = email
        self._active  = True

    @property
    def active(self) -> bool:
        return self._active

    def deactivate(self) -> None:
        self._active = False

    def to_dict(self) -> dict:
        return {
            "id":       self.id,
            "username": self.username,
            "email":    self.email,
            "active":   self._active,
        }

    def __repr__(self) -> str:
        return f"User(id={self.id}, username={self.username!r})"

alice = User(1, "민준", "[email protected]")
print(alice.to_dict())
alice.deactivate()
print(alice.active)   # False

학습/검증 분할을 프로퍼티 뒤에 캡슐화하고, 깔끔한 디버그 출력을 위한 __repr__을 가진 DataSplit 클래스:

python
class DataSplit:
    def __init__(self, data: list, train_ratio: float = 0.8):
        split       = int(len(data) * train_ratio)
        self._train = data[:split]
        self._val   = data[split:]

    @property
    def train(self) -> list:
        return self._train

    @property
    def val(self) -> list:
        return self._val

    @property
    def sizes(self) -> tuple[int, int]:
        return len(self._train), len(self._val)

    def __repr__(self) -> str:
        return f"DataSplit(train={len(self._train)}, val={len(self._val)})"

data  = list(range(100))
split = DataSplit(data, train_ratio=0.8)
print(split)         # DataSplit(train=80, val=20)
print(split.sizes)   # (80, 20)

_train_val의 밑줄 접두사는 호출자가 원시 리스트를 직접 변경하기보다 프로퍼티를 통해 접근해야 한다는 신호를 보냅니다. Python은 이를 강제하지 않지만, 명확한 계약을 설정합니다.