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

클래스와 객체

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

클래스는 사용자 정의 타입을 만드는 메커니즘입니다. 클래스는 하나의 틀을 정의합니다. 각 인스턴스가 가지는 데이터(값이 저장되는 속성, attribute)와 그것이 지원하는 동작(거기에 붙은 함수인 메서드)이 그 틀에 포함됩니다. 값들을 여러 변수에 나눠 담고 여기저기 넘겨가며 추적하는 대신, 이를 명확한 인터페이스를 가진 하나의 객체로 묶을 수 있습니다.

class 문은 새로운 타입(str이나 int와 같은 종류의, 사용자 정의 값의 종류)을 만들고, 다른 대입문과 마찬가지로 그것을 이름에 바인딩합니다. 클래스를 호출하면 인스턴스(그 타입의 구체적인 객체 하나)를 만들 수 있습니다: Player("Alice"). 이 호출 뒤에서 파이썬은 빈 객체를 할당하고 __init__을 실행해 내용을 채웁니다. 이 모델은 끝까지 일관됩니다. 클래스 자체도 변수에 저장하거나 함수에 전달하거나 런타임(프로그램이 실행되는 동안)에 조사할 수 있는 객체이며, 바로 이 점 덕분에 이 챕터 뒷부분에서 다룰 대안 생성자와 레지스트리가 가능해집니다.

설계도와 인스턴스

클래스는 설계도입니다. 인스턴스는 그 설계도로 만들어진 구체적인 물건입니다. 필요한 만큼 인스턴스를 만들 수 있으며, 각 인스턴스는 자기만의 데이터를 가지지만 클래스에 정의된 메서드는 함께 공유합니다.

클래스는 구조와 동작을 정의합니다. 인스턴스는 그 클래스로부터 생성된 객체입니다. 각 인스턴스는 자기만의 데이터를 가지지만 클래스의 메서드 객체는 공유합니다. 인스턴스를 만드는 것은 클래스를 함수처럼 호출하는 것입니다: Dog()는 새로운 Dog 인스턴스를 만듭니다.

클래스를 호출하면 순서대로 두 가지 일이 일어납니다. 먼저 빈 인스턴스를 할당하고, 그런 다음 __init__을 실행해 데이터를 채웁니다. 완성된 객체는 자신의 클래스를 기억하고 있으며(type(rex)로 다시 확인할 수 있습니다), 바로 이 연결 덕분에 rex.bark()를 호출했을 때 파이썬이 메서드를 찾을 수 있습니다. 인스턴스에서 먼저 찾고, 그다음 클래스에서 찾습니다. 실무적으로 중요한 점은 메서드는 (클래스에) 한 곳에만 존재하며 공유되는 반면 데이터는 인스턴스마다 따로 있다는 것입니다. 그래서 Dog 객체가 천 개 있으면 데이터도 천 벌이 필요하지만, 각 메서드는 단 하나의 사본만 있으면 됩니다.

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

rex = Dog()
luna = Dog()

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

Dog는 클래스입니다. rexluna는 인스턴스입니다. 서로 다른 두 마리 개이지만, 클래스에 정의된 같은 동작을 공유합니다.

Juno설계도와 인스턴스 클래스는 설계도이고, 인스턴스는 그로부터 만들어진 하나의 물건입니다. 클래스를 Dog()처럼 함수로 호출하면 새로운 인스턴스가 만들어집니다. 모든 인스턴스는 클래스의 메서드를 공유하지만 자기만의 데이터를 유지하므로, rexluna는 같은 방식으로 행동하면서도 서로 다른 개일 수 있습니다.
Juno설계도와 인스턴스Dog()를 호출하면 인스턴스가 만들어집니다. 자기만의 데이터와, 클래스가 공유하는 메서드를 함께 갖게 되죠. 이렇게 묶어두는 것이 핵심입니다. 관련된 값과 그것을 다루는 코드가 따로따로 손으로 넘겨야 하는 변수가 아니라 함께 다니게 됩니다.
Juno설계도와 인스턴스 클래스를 호출하면 빈 객체가 할당되고, 그 위에서 __init__이 실행됩니다. 메서드는 클래스에 한 번만 존재하며 공유되고, 데이터는 인스턴스마다 따로 있습니다. 그래서 rex에 대한 메서드 호출은 인스턴스를 먼저 살핀 다음 클래스를 살펴서 찾아집니다. 이 구분 덕분에 인스턴스가 만 개여도 비용이 크지 않습니다.

__init__self

__init__은 새 인스턴스를 만들 때 파이썬이 자동으로 호출하는 메서드입니다. 여기서 객체의 초기 데이터를 설정합니다. self는 메서드가 자신이 다루고 있는 특정 인스턴스를 가리키는 방법이며, 항상 첫 번째 매개변수입니다.

__init__은 방금 할당된 인스턴스를 초기화합니다. self는 모든 인스턴스 메서드의 첫 번째 매개변수에 붙이는 관례적인 이름입니다. alice.display()를 호출하면 파이썬이 자동으로 인스턴스를 전달합니다. __init__ 안에서 self에 설정하는 속성은 인스턴스 속성입니다. 각 인스턴스는 자기만의 사본을 갖습니다.

__init__은 이미 할당된 인스턴스 위에서 실행되어 그것을 설정합니다. self는 키워드가 아니라 첫 번째 매개변수이며, 파이썬이 대신 채워줍니다. alice.display()를 호출하면 파이썬은 뒤에서 aliceself로 전달합니다. self.attr = value로 설정하면 그 값은 오직 그 하나의 인스턴스에 기록되므로, 두 Player 객체가 서로의 데이터를 침범하는 일은 없습니다. 더 엄격한 언어를 쓰던 사람들이 놀라는 점 하나: 파이썬은 __init__뿐 아니라 어디서든 속성을 추가하도록 허용합니다. 그래서 self.scor = 0처럼 오타가 나면 에러를 내는 대신 새로운 속성을 만들어버립니다. 인스턴스가 앞으로 사용할 모든 속성은 None으로라도 __init__에서 미리 설정해두세요. 그러면 객체의 형태가 한 곳에 선언되고, 다른 곳의 오타는 눈에 띄게 됩니다.

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("Alice")
bob = Player("Bob", score=50)

alice.add_points(30)
alice.display()   # "Alice: 30 points"
bob.display()     # "Bob: 50 points"

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

Juno__init__과 self__init__은 인스턴스를 만드는 순간 실행되므로, 여기서 self.name = value처럼 초기 데이터를 설정합니다. self는 파이썬이 다루고 있는 인스턴스이며, 항상 메서드의 첫 번째 매개변수로 자동으로 전달됩니다. alice.display()를 호출할 때 self를 직접 전달할 필요는 없습니다.
Juno__init__과 self__init__은 새로 만들어진 인스턴스를 설정하며, 여기서 self에 저장하는 것은 무엇이든 인스턴스 속성이 되어 객체마다 하나씩 사본을 갖습니다. self는 모든 인스턴스 메서드의 첫 번째 매개변수입니다. alice.display()라고 쓰면 파이썬이 대신 인스턴스를 전달해주므로, 정의할 때만 이름을 붙이면 됩니다.
Juno__init__과 selfself는 인스턴스이고, 모든 호출에서 자동으로 전달되며, self.attr에 쓴 값은 그 하나의 객체에 남습니다. 파이썬은 __init__ 바깥에서 속성을 추가하는 것도 막지 않으므로, 오타가 난 self.scor는 조용히 새 속성을 만들어버립니다. __init__에서 모든 속성을 None으로라도 선언해두면 오타가 숨을 곳이 없어집니다.

메서드

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

인스턴스 메서드는 클래스에 저장된 일반 함수입니다. instance.method에 접근하면 파이썬은 바운드 메서드(bound method)를 돌려줍니다. 같은 함수이지만 인스턴스가 이미 self로 연결되어 있어서, 나머지 인자만 넘기면 됩니다. 메서드에서 self를 반환하면 체이닝이 가능해집니다: obj.scale(2).rotate(90).

메서드는 클래스에 있는 하나의 함수이며 모든 인스턴스가 공유합니다. c.scale에 접근한다고 해서 원시 함수 자체를 얻는 것이 아니라, 바운드 메서드를 얻습니다. 같은 함수이지만 c가 이미 self로 연결되어 있는 것이죠. 이것이 여러분이 직접 하지 않아도 self가 전달되는 이유입니다. 실전 코드에서 유용한 패턴이 두 가지 있습니다. 상태를 변경하는 메서드에서 self를 반환하면 플루언트 인터페이스(연결된 호출, 예를 들면 c.scale(2).scale(0.5))를 얻을 수 있습니다. 다만 이것은 체이닝이 자연스럽게 읽히는 빌더 스타일 객체에만 적용하세요. self를 반환하는 메서드와 새 값을 반환하는 메서드는 호출하는 쪽에서 보면 겉모습이 똑같기 때문입니다. 그리고 바운드 메서드는 일급 값이므로, 하나를 변수에 저장했다가(handler = obj.save) 나중에 호출할 수도 있습니다. 콜백과 이벤트 핸들러가 바로 이 성질에 의존합니다.

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
Juno메서드 메서드는 클래스 안에 정의된 함수이며, 첫 번째 매개변수는 항상 self, 즉 그것이 다루고 있는 인스턴스입니다. 파이썬이 self를 대신 전달해주므로 c.area()를 호출할 때 추가로 넘길 것은 없습니다. 메서드는 self를 통해 그 객체 자신의 데이터를 읽고 바꿉니다.
Juno메서드 메서드는 클래스에 있는 평범한 함수이며, 인스턴스를 통해 접근하면 self가 자동으로 연결되므로 나머지 인자만 넘기면 됩니다. c.scale(2).scale(0.5)처럼 체이닝하고 싶다면 self를 반환하세요. 그렇지 않다면 호출한 쪽이 원하는 값을 반환하면 됩니다.
Juno메서드obj.method는 바운드 메서드를 돌려줍니다. objself로 이미 연결된 함수라서, 여러분이 직접 self를 넘길 필요가 없는 것이죠. self를 반환하면 플루언트한 체이닝을 얻을 수 있고, 빌더 객체라면 그럴 가치가 있지만 모든 메서드가 그렇게 하면 오히려 헷갈립니다. 그리고 바운드 메서드도 값이므로 handler = obj.save처럼 저장해뒀다가 나중에 콜백으로 쓸 수 있습니다.

클래스 변수와 인스턴스 변수

클래스에 직접 정의된 변수(즉 __init__ 안이 아닌 곳에)는 클래스 변수입니다. 모든 인스턴스는 같은 클래스 변수를 공유합니다. __init__ 안에서 self에 설정한 변수는 인스턴스 변수이며, 각 객체마다 고유합니다.

클래스 변수는 클래스에 한 번만 저장되어 모든 인스턴스가 공유합니다. 인스턴스 변수는 개별 객체에 저장됩니다. self.attr을 읽으면 파이썬은 먼저 인스턴스를 확인하고, 그다음 클래스를 확인합니다. self.attr = value로 쓰면 항상 그 인스턴스 자신의 사본을 만들거나 갱신하며, 오직 그 객체에 한해서 클래스 변수를 가려버립니다.

self.attr을 읽으면 파이썬은 인스턴스를 먼저 확인하고, 그다음 클래스를 확인합니다. 그래서 클래스 변수는 어떤 인스턴스가 자기만의 값을 써서 그것을 가리기 전까지는 공유되는 기본값처럼 동작합니다. 실제 서비스에서 자주 물리는 함정은 가변(mutable, 제자리에서 값을 바꿀 수 있는, 리스트나 딕셔너리 같은) 클래스 변수입니다. self.x = ...처럼 대입하면 개인 사본이 만들어지지만, self.x.append(...)는 그렇지 않습니다. 공유되는 그 하나의 객체에 접근하는 것이므로 모든 인스턴스가 그 변화를 보게 됩니다. 인스턴스마다 새로운 리스트를 원한다면, __init__에서 self.history = []로 만드세요. 클래스 레벨에서 history = []로 만들면 안 됩니다. 클래스 변수는 정말로 공유되어야 하는 것에만 쓰고, 상수, 기본값, 카운터처럼 불변인 것으로 유지하세요.

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("Alice")
bob = Player("Bob")

Player.max_lives = 5    # 현재와 앞으로의 모든 인스턴스에 대해 변경됩니다

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

Juno클래스 변수와 인스턴스 변수 클래스에 바로 설정된 변수는 모든 인스턴스가 공유하고, __init__에서 self에 설정된 변수는 그 하나의 객체에만 속합니다. self.attr = value로 쓰면 항상 그 인스턴스 자신의 사본을 만들거나 갱신합니다. 그러니 모두에게 같은 값이면 클래스 변수를, 객체마다 다르면 인스턴스 변수를 쓰세요.
Juno클래스 변수와 인스턴스 변수self.attr을 읽으면 인스턴스를 확인한 다음 클래스를 확인하므로, 클래스 변수는 공유되는 기본값 역할을 합니다. self.attr = value로 쓰면 항상 인스턴스에 자리 잡고, 그 객체에 한해 클래스 쪽 값을 가립니다. 상수와 기본값은 클래스 변수로, 객체별 상태는 self에 두세요.
Juno클래스 변수와 인스턴스 변수 조용히 숨어있는 버그는 가변 클래스 변수입니다. self.x = ...는 개인 사본을 만들지만 self.x.append(...)는 공유되는 그 하나의 객체를 바꾸므로 모든 인스턴스가 그것을 보게 됩니다. 인스턴스마다 리스트가 필요하면 __init__에서 self.history = []로 만드세요, 클래스 레벨의 history = []는 절대 안 됩니다.

__str____repr__

__str__print()와 f-string이 객체를 어떻게 보여줄지 결정합니다. __repr__은 콘솔이나 디버깅에서 보이는 개발자용 표시를 결정합니다. 항상 __repr__을 정의하세요. 디버그용 표시와 별도로 깔끔한 사용자용 화면이 필요할 때 __str__을 정의하세요.

__str__str(), print(), 그리고 출력 챕터에서 다룬 f-string이 호출하는, 사용자에게 보여주는 텍스트입니다. __repr__repr()이 호출하고 객체가 콘솔에 그대로 출력될 때 보이는, 개발자용 화면입니다. __repr__만 정의되어 있으면 파이썬은 두 경우 모두 그것을 사용합니다. 관례는 이렇습니다. __repr__은 객체를 다시 만들 수 있는 코드처럼 보이는 문자열을 반환하고, __str__은 읽기 쉬운 요약을 반환합니다.

이 둘은 던더(dunder, double underscore의 줄임말로, 앞뒤에 __가 붙어서 파이썬이 적절한 순간에 알아서 호출해주는 메서드) 메서드입니다. print(obj)str(obj)__str__을 호출하고, __str__이 없으면 __repr__로 대체합니다. repr(obj)나 객체가 콘솔에 맨몸으로 표시될 때는 __repr__을 호출합니다. 그래서 항상 할 일이 있는 쪽은 __repr__이며, 이것이 바로 "항상 __repr__을 정의하고, 사용자용 텍스트가 디버그 텍스트와 달라야 할 때만 __str__을 추가하라"는 규칙의 이유입니다. __repr__은 그 객체를 만든 생성자 호출처럼 보이게 만들고(Player(name='Alice', score=87)), 필드에는 !r을 쓰세요. 그러면 문자열은 따옴표를 유지하고, 우연히 들어간 공백이나 줄바꿈이 숨지 않고 드러납니다. 새벽 2시에 로그 한 줄이나 스택 트레이스를 읽을 수 있게 만드는 것이 바로 좋은 __repr__입니다.

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("Alice", 87)
print(alice)        # "Alice (87 pts)"   (__str__ 사용)
repr(alice)         # "Player(name='Alice', score=87)"  (__repr__ 사용)

항상 __repr__을 정의하세요. 디버그용 화면과 별도로 깔끔한 사용자용 표현이 필요할 때 __str__을 정의하세요. __repr__만 정의되어 있으면 파이썬은 두 경우 모두 그것을 사용합니다.

Juno__str__과 __repr____str__print()와 f-string이 보여주는 친절한 버전입니다. __repr__은 콘솔에서 보게 되는 개발자용 화면입니다. 항상 __repr__을 작성하세요. __str__을 깜빡해도 일할 준비가 되어 있는 쪽이 바로 이겁니다. __str__은 사용자용 텍스트가 다르게 읽혀야 할 때만 추가하세요.
Juno__str__과 __repr____str__print()와 f-string을 위한 읽기 좋은 버전이고, __repr__은 디버그용 버전이며, __str__이 없으면 파이썬은 __repr__로 대체합니다. 항상 __repr__을 정의하고, 그것을 그 객체를 만든 호출처럼 보이게 만드세요. 그러면 나중에 로그에 객체가 등장할 때 스스로를 구하게 됩니다.
Juno__str__과 __repr____repr__은 항상 일할 준비가 되어 있고, __str__은 사용자용 화면이 달라야 할 때만 필요합니다. 그러니 __repr__부터, 매번 정의하세요. 생성자 모양으로 만들고 필드에는 !r을 붙이면 우연히 들어간 공백이나 줄바꿈이 숨지 않고 드러납니다. 이 한 가지 습관이 새벽 2시의 스택 트레이스를 읽을 수 있게 만듭니다.

비공개 관례

파이썬에는 진짜 비공개 변수가 없지만, 이름 앞에 밑줄 하나를 붙이는 것(_balance)은 "이것은 내부용이니 클래스 바깥에서 직접 사용하지 말라"는 신호를 보내는 관례입니다. 언어가 강제하는 것이 아니라 다른 개발자에게 전하는 의사소통입니다.

밑줄 하나(_attr)는 내부용임을 알리는 관례입니다. 파이썬이 이를 강제하지는 않지만, 모든 린터, IDE, 개발자가 이를 존중합니다. 밑줄 두 개(__attr)는 이름 맹글링(name mangling)을 유발합니다. 파이썬은 이것을 _ClassName__attr로 다시 씁니다. 이는 서브클래스에서 우연히 충돌하는 것을 막아줍니다. 진짜 비공개가 아니라 충돌 방지 메커니즘입니다.

밑줄 하나는 순전히 관례일 뿐입니다. 아무것도 이를 강제하지 않지만, 린터(코드 스타일과 있음직한 버그를 검사하는 도구), 편집기, 리뷰어 모두 _balance를 "내부용이니 바깥에서 건드리지 말라"는 뜻으로 읽습니다. 밑줄 두 개는 이름 맹글링을 유발합니다. 파이썬은 __attr_ClassName__attr로 다시 써서, 서브클래스가 자신의 __attr을 정의해도 부모의 것과 우연히 충돌하지 않게 합니다. 이것은 비공개가 아니라 충돌 방지 장치입니다. 누군가 정말 마음먹으면 맹글링된 이름도 바깥에서 여전히 접근할 수 있습니다. 널리 서브클래싱될 목적의 기반 클래스에서, 서브클래스들이 건드릴 수 없어야 할 속성이 필요할 때만 이것을 쓰세요. 보통 코드에서는 밑줄 하나가 올바른 기본 선택이고, 밑줄 두 개는 대개 혼란만 만듭니다. 앞서 나온 오타 self.scor처럼 임의의 속성이 아예 설정되지 못하게 막고 싶다면, 고정된 속성 이름 목록으로 __slots__를 정의하세요. 그러면 파이썬에게 인스턴스가 가질 수 있는 속성은 오직 그것들뿐이라고 알려주는 것이 되어, 다른 대입은 조용히 붙는 대신 에러를 냅니다. 덤으로 인스턴스당 메모리도 줄어듭니다.

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)는 이름 맹글링을 유발합니다. 파이썬은 서브클래스에서의 충돌을 피하기 위해 그 속성 이름을 _ClassName__name으로 바꿉니다. 이는 좀처럼 필요하지 않습니다. 대부분의 코드에서는 밑줄 하나가 관례입니다.

Juno비공개 관례 파이썬에는 진짜 비공개 변수가 없지만, 앞에 붙는 밑줄 하나(_balance)는 "내부용이니 바깥에서는 건드리지 말라"는 합의된 신호입니다. 아무것도 여러분이 접근하는 것을 막지 않습니다. 이것은 다른 개발자들, 그리고 미래의 여러분에게 보내는 메시지입니다. 밑줄 두 개는 서브클래스에서 이름 충돌을 피하기 위한 더 드문 도구이고, 일상적으로는 밑줄 하나를 쓰게 될 것입니다.
Juno비공개 관례 밑줄 하나는 "내부용"이라는 뜻이며, 파이썬이 강제하지 않아도 모든 도구와 리뷰어가 이를 존중합니다. 밑줄 두 개는 _ClassName__attr로 이름 맹글링을 유발해 서브클래스 이름 충돌을 막아주지만, 접근을 막지는 않습니다. 기본적으로 밑줄 하나를 쓰고, 밑줄 두 개는 드물게만 쓰세요.
Juno비공개 관례 밑줄 하나는 관례이고, 밑줄 두 개는 _ClassName__attr로 맹글링되어 서브클래스 충돌을 막아줍니다. 둘 다 진짜 비공개는 아닙니다. __attr은 널리 서브클래싱되는 기반 클래스에만 두세요. 그 외에는 혼란만 늘어납니다. 임의의 속성 추가를 아예 금지하고 오타난 self.scor를 잡아내고 싶다면, 고정된 목록으로 __slots__를 설정하세요.

상속

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

상속은 "~는 ~이다"(is-a) 관계를 모델링합니다. DogAnimal입니다. 서브클래스는 부모의 모든 메서드와 속성을 얻으며, 자신만의 버전을 정의해 그중 무엇이든 오버라이드할 수 있습니다. 메서드를 호출하면 파이썬은 서브클래스에서 먼저 찾고 그다음 부모 쪽으로 올라가므로, 오버라이드되지 않은 메서드는 자동으로 그대로 통과합니다. 이 탐색 경로가 메서드 결정 순서(method resolution order, MRO), 즉 파이썬이 검색하는 클래스들의 순서 있는 목록입니다.

obj.method를 호출하면 파이썬은 정해진 순서, 즉 MRO(method resolution order: 어떤 클래스와 그 조상들을 평평하게 편 목록으로, Dog.__mro__로 읽어볼 수 있습니다)로 클래스들을 검색해 처음 일치하는 것을 사용합니다. 단일 상속이라면 서브클래스 다음 부모, 이렇게 단순합니다. MRO가 존재하는 이유는 다중 상속과 다이아몬드 경우(공통 조부모로부터 함께 상속받는 두 부모가 있는 경우) 때문입니다. 파이썬은 조부모가 두 부모 모두의 뒤에 딱 한 번만 나타나도록 클래스 순서를 정해서, 호출을 결정론적으로 처리합니다. 실무적인 조언은 이렇습니다. 얕은 계층 구조와 단일 상속을 선호하세요. 깊거나 다중 상속인 트리는 "실제로 어떤 메서드가 실행됐는가"를 파악하기 어렵게 만들기 때문입니다. 여러 베이스를 정말로 섞어 쓸 때는, 다음 절에서 다룰 super()를 통해 모든 __init__이 그 순서를 따라 정확히 한 번씩 실행되도록 하세요.

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__을 상속받으므로, 자기만의 __init__이 필요 없습니다. 이들은 speak()만 자신의 동작으로 오버라이드합니다.

Juno상속 서브클래스는 부모가 가진 모든 것을 상속받은 다음, 바꾸고 싶은 메서드만 오버라이드합니다. 오버라이드하지 않은 것은 그냥 부모의 것을 그대로 이어받습니다. 바로 이것이 핵심입니다. 공유되는 동작은 한 번만 작성하고, 다른 부분만 특화하세요. DogCatAnimal__init__을 재사용하고 speak()만 다시 정의합니다.
Juno상속 상속은 "~는 ~이다"입니다. 서브클래스는 부모의 메서드를 얻고 필요한 것만 오버라이드하며, 오버라이드되지 않은 호출은 그대로 통과합니다. 파이썬은 MRO를 따라가며 메서드를 찾습니다. 서브클래스 먼저, 그다음 부모로. 공유되는 기반은 재사용하고, 다른 부분만 다시 정의하세요.
Juno상속 파이썬은 MRO를 따라가며 처음 일치하는 것으로 메서드를 결정하는데, 이 덕분에 다이아몬드 경우도 결정론적으로 처리됩니다. 계층 구조는 얕게 유지하고 단일 상속에 기대세요. 깊거나 다중 상속인 트리는 "어떤 메서드가 실행됐는지"를 알아맞히는 게임으로 만들어버립니다. 여러 베이스를 섞어야 한다면, 모든 __init__super()로 연결하세요.

super()

super()는 부모 클래스의 메서드를 호출합니다. 부모의 동작을 완전히 대체하는 것이 아니라 확장하고 싶을 때 사용하세요. 부모의 __init__을 호출해 그 초기화를 실행한 다음, 서브클래스에 필요한 것을 추가로 더하는 식입니다.

super()는 MRO에서 다음 클래스로 메서드 호출을 위임하는 프록시 객체를 반환합니다. 부모가 __init__을 가지고 있다면, 서브클래스의 __init__에서 항상 super().__init__()을 호출하세요. 이를 빼먹으면 부모의 초기화 코드가 실행되지 않으며, 그 결과 객체가 온전하지 않은 상태로 남을 수 있습니다.

super()는 "나의 직접 부모"를 뜻하는 것이 아니라 "MRO(앞 절에서 다룬 탐색 순서)에서 다음 클래스"를 뜻합니다. 단순한 서브클래스에서는 그 다음 클래스가 곧 부모이므로 이 구분이 학문적으로만 보일 수 있지만, 바로 이 점이 다중 상속을 작동하게 만드는 핵심입니다. 트리 안의 모든 클래스가 super().__init__(...)을 호출한다면, 각 __init__은 MRO 순서대로 한 번씩만 실행되며, 어떤 클래스도 부모를 이름으로 하드코딩하지 않게 됩니다. 이것이 바로 Animal.__init__(self)가 아니라 super().__init__()을 써야 하는 이유입니다. 부모를 직접 이름으로 지정하면 다른 클래스가 믹스인되는 순간 그 연결 고리가 끊어지고, 공유되는 베이스가 두 번 실행될 수도 있습니다. 인자 없는 super()가 요즘 쓰는 형태이고, 예전 방식인 super(Dog, self)는 같은 것을 풀어 쓴 것으로, 예전 버전을 대상으로 하는 코드에서는 여전히 볼 수 있습니다.

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__()을 호출하세요.

Junosuper()super()는 부모 클래스에 접근합니다. 그래서 super().__init__()은 여러분이 자신의 것을 추가하기 전에 부모의 초기화를 실행합니다. 서브클래스가 자기만의 __init__을 작성하고 부모도 하나 가지고 있을 때 이것을 사용하세요. 빼먹으면 부모의 초기화가 실행되지 않아 객체가 절반만 만들어진 채로 남습니다.
Junosuper()super()는 그 위에 있는 다음 클래스로 위임하므로, 부모가 __init__을 가지고 있다면 서브클래스의 __init__에서 super().__init__()을 호출하세요. 이를 잊으면 부모의 초기화가 건너뛰어져 객체가 온전하지 않은 상태로 남을 수 있습니다. 이것이 바로 서브클래스를 그것이 확장하는 부모와 연결해주는 한 줄입니다.
Junosuper()super()는 "나의 부모"가 아니라 "MRO의 다음 클래스"를 뜻합니다. 그래서 Animal.__init__(self)처럼 하드코딩하면 어떤 클래스가 믹스인되는 순간 그 연결 고리가 끊어집니다. 모든 __init__super()를 호출한다면, 각각은 순서대로 한 번씩만 실행됩니다. 인자 없는 super()가 요즘 방식이고, super(Dog, self)는 같은 것을 풀어 쓴 것입니다.

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

@classmethod는 인스턴스 대신 클래스 자체를 전달받는 메서드를 만듭니다. 대안 생성자, 즉 문자열이나 파일, 다른 형식으로부터 인스턴스를 만들 때 유용합니다. @staticmethod는 정리 목적으로 클래스 안에 놓인 평범한 함수입니다. 인스턴스도 클래스도 전달받지 않습니다.

@classmethod는 인스턴스가 아니라 cls(클래스)를 첫 번째 인자로 전달받습니다. 주된 용도는 서로 다른 입력 형식으로부터 인스턴스를 만드는 대안 생성자입니다. @staticmethod는 클래스 아래에 네임스페이스만 붙은 일반 함수입니다. 클래스나 인스턴스에 접근할 수 없습니다. 생성자에는 @classmethod를, 클래스와 논리적으로 연결된 유틸리티 함수에는 @staticmethod를 사용하세요.

@classmethod는 인스턴스 대신 클래스를 첫 번째 인자(cls)로 전달받는데, 정말 쓸모 있는 세부 사항은 cls가 그 메서드가 정의된 클래스가 아니라 그 메서드가 호출된 클래스라는 점입니다. 그래서 Player.from_stringcls(...)로 객체를 만들고, 서브클래스인 ProPlayerProPlayer.from_string(...)을 호출하면, 이것은 Player가 아니라 ProPlayer를 만듭니다. 바로 이것이 대안 생성자가 클래스 이름을 하드코딩하지 않고 @classmethod를 쓰는 이유입니다. 상속 하에서도 계속 작동하기 때문이죠. @staticmethod는 인스턴스도 클래스도 받지 않습니다. 그 로직이 클래스와 관련이 있지만 클래스의 어떤 데이터도 필요하지 않을 때, 네임스페이스 정리를 위해 클래스 안에 놓인 평범한 함수입니다. 다른 형식으로부터 인스턴스를 만들 때는 classmethod를, 관련된 헬퍼에는 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("Alice,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("Alice")   # True
Player.is_valid_name("A1")      # False

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

Juno클래스 메서드와 정적 메서드@classmethod는 인스턴스 대신 클래스를 전달받으므로, 문자열이나 파일 등 어떤 형식으로부터든 Player를 만드는 대안 생성자에 딱 맞습니다. @staticmethod는 정리를 위해 클래스 안에 넣어둔 평범한 함수이며, 클래스도 인스턴스도 받지 않습니다. 일반 메서드는 하나의 객체 데이터를 다루지만, 이 둘은 그렇지 않습니다.
Juno클래스 메서드와 정적 메서드@classmethod는 클래스 자체인 cls를 받으므로, cls(...)를 반환하는 대안 생성자에 사용하세요. @staticmethod는 암묵적인 것을 아무것도 받지 않는, 클래스 아래 네임스페이스가 붙은 헬퍼입니다. 객체별 데이터에는 일반 메서드, 인스턴스를 만들 때는 classmethod, 관련된 유틸리티에는 staticmethod입니다.
Juno클래스 메서드와 정적 메서드 classmethod 생성자의 핵심은 cls가 그것을 호출한 쪽이라는 점입니다. 그래서 ProPlayer.from_string(...)Player가 아니라 ProPlayer를 만듭니다. 클래스 이름을 하드코딩하면 이 이점을 잃습니다. 생성할 때는 classmethod, 데이터가 필요 없는 관련 헬퍼는 staticmethod, 인스턴스를 다루는 모든 것은 일반 메서드입니다.

@property

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

@property는 메서드를 읽기 전용 속성으로 바꿔줍니다. 그 속성에 접근할 때 메서드가 실행됩니다. 이는 저장된 데이터로부터 계산되는 값에, 그리고 공개 인터페이스를 바꾸지 않으면서 속성 접근에 검증을 추가하는 데 유용합니다. 짝을 이루는 @name.setter는 그 속성을 쓸 수 있게 만들어줍니다.

@property는 메서드를 속성처럼 읽는 무언가로 바꿔줍니다. c.area처럼 괄호 없이, 접근할 때마다 그 메서드가 실행됩니다. @area.setter를 추가하면 쓸 수도 있게 되며, 이때 들어오는 값에 대해 여러분의 코드가 실행됩니다. 바로 여기서 검증이 이루어집니다(저장되기 전에 음수 반지름을 거부하는 식으로). 진짜 가치는 이것이 호출하는 쪽에는 아무것도 바꾸지 않는다는 점입니다. 평범한 self.radius 속성으로 시작했다가, 나중에 읽고 쓰는 코드는 한 줄도 건드리지 않고 검증이 있는 프로퍼티로 승격시킬 수 있습니다. 그래서 파이썬에는 게터/세터 의식이 없는 것입니다. 평범한 속성을 그대로 노출하고, 계산이나 검증이 필요할 때만 프로퍼티를 쓰는 것이죠. 실무에서 주의할 점 두 가지가 있습니다. 접근이 공짜처럼 보이지만 실제로는 코드가 실행되므로 가볍게 유지하세요(매번 다시 계산하는 대신 비용이 큰 결과는 캐시하세요). 그리고 프로퍼티가 조용히 무거운 작업을 하거나 예외를 던지면, 그것을 단순한 필드라고 생각한 사람을 놀라게 합니다.

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

프로퍼티는 계산되는 값, 즉 다른 속성으로부터 유도되어 () 없이 접근하는 것이 자연스러운 값에 유용합니다.

Juno@property@property는 메서드를 괄호 없이 속성처럼 읽게 해줍니다. c.area() 대신 c.area인 것이죠. 다른 속성으로부터 계산되고, 그냥 데이터처럼 읽히는 것이 자연스러운 값에 잘 맞습니다. 뒤에서는 여전히 접근할 때마다 여러분의 메서드가 실행됩니다.
Juno@property@property는 메서드를 속성처럼 읽게 해주고, 짝을 이루는 @name.setter는 쓸 수 있게 만들어서 들어오는 값을 검증할 수 있게 해줍니다. 이점은, 평범한 속성을 나중에 아무 호출부도 바꾸지 않고 프로퍼티로 바꿀 수 있다는 것입니다. 계산되거나 검증이 필요한 값에 사용하고, 모든 필드에 습관처럼 쓰지는 마세요.
Juno@property@property를 좋아할 이유는 이렇습니다. 평범한 속성을 나중에 계산되거나 검증되는 것으로 바꿔도 호출부는 전혀 바뀌지 않으므로, 정말 필요해지기 전까지는 게터/세터를 만들지 않아도 됩니다. 접근이 공짜처럼 보이지만 실제로 코드가 실행되니 가볍게 유지하고, 비용이 큰 것은 캐시하며, 필드처럼 보이는 것 뒤에 무거운 작업이나 예외를 숨기지 마세요.

실전 활용

인스턴스 속성, 메서드, @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")
alice.earn_points(50)
alice.take_hit()
print(alice)            # "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, "alice", "[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에 붙은 밑줄 접두사는 호출하는 쪽이 원시 리스트를 직접 변경하지 말고 프로퍼티를 거쳐야 한다는 신호를 보냅니다. 파이썬이 이를 강제하지는 않지만, 명확한 계약을 정해줍니다.