클래스와 객체
지금까지 다뤄온 모든 타입(문자열, 리스트, 딕셔너리)은 사실 클래스입니다. "hello".upper()를 호출할 때, 여러분은 문자열 객체의 메서드를 호출하고 있는 것입니다. 클래스를 사용하면 자신만의 데이터와 동작을 가진 타입을 직접 정의할 수 있습니다. Player 클래스는 이름, 점수, 레벨을 담을 수 있고, 자기 자신을 어떻게 표시할지도 알고 있습니다.
설계도와 인스턴스
클래스는 설계도입니다. 인스턴스는 그 설계도로 만들어진 구체적인 물건입니다. 필요한 만큼 인스턴스를 만들 수 있으며, 각 인스턴스는 자기만의 데이터를 가지지만 클래스에 정의된 메서드는 함께 공유합니다.
class Dog:
def bark(self):
print("Woof!")
rex = Dog()
luna = Dog()
rex.bark() # "Woof!"
luna.bark() # "Woof!"Dog는 클래스입니다. rex와 luna는 인스턴스입니다. 서로 다른 두 마리 개이지만, 클래스에 정의된 같은 동작을 공유합니다.
Dog()처럼 함수로 호출하면 새로운 인스턴스가 만들어집니다. 모든 인스턴스는 클래스의 메서드를 공유하지만 자기만의 데이터를 유지하므로, rex와 luna는 같은 방식으로 행동하면서도 서로 다른 개일 수 있습니다. __init__과 self
__init__은 새 인스턴스를 만들 때 파이썬이 자동으로 호출하는 메서드입니다. 여기서 객체의 초기 데이터를 설정합니다. self는 메서드가 자신이 다루고 있는 특정 인스턴스를 가리키는 방법이며, 항상 첫 번째 매개변수입니다.
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.name과 self.score는 인스턴스 속성입니다. 클래스 자체가 아니라 특정 객체에 속합니다. 각 Player 인스턴스는 자기만의 name과 score를 가집니다.
__init__은 인스턴스를 만드는 순간 실행되므로, 여기서 self.name = value처럼 초기 데이터를 설정합니다. self는 파이썬이 다루고 있는 인스턴스이며, 항상 메서드의 첫 번째 매개변수로 자동으로 전달됩니다. alice.display()를 호출할 때 self를 직접 전달할 필요는 없습니다. 메서드
클래스 안에 정의된 함수는 모두 메서드입니다. 인스턴스 메서드는 항상 첫 번째 매개변수로 self를 가지며, 파이썬이 자동으로 전달합니다. 메서드는 self를 통해 인스턴스의 데이터를 읽고 바꿀 수 있습니다.
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.159self, 즉 그것이 다루고 있는 인스턴스입니다. 파이썬이 self를 대신 전달해주므로 c.area()를 호출할 때 추가로 넘길 것은 없습니다. 메서드는 self를 통해 그 객체 자신의 데이터를 읽고 바꿉니다. 클래스 변수와 인스턴스 변수
클래스에 직접 정의된 변수(즉 __init__ 안이 아닌 곳에)는 클래스 변수입니다. 모든 인스턴스는 같은 클래스 변수를 공유합니다. __init__ 안에서 self에 설정한 변수는 인스턴스 변수이며, 각 객체마다 고유합니다.
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 # 현재와 앞으로의 모든 인스턴스에 대해 변경됩니다모든 인스턴스가 공유하는 값(상수, 카운터, 기본값)에는 클래스 변수를 사용하세요. 객체마다 다른 데이터에는 인스턴스 변수를 사용하세요.
__init__에서 self에 설정된 변수는 그 하나의 객체에만 속합니다. self.attr = value로 쓰면 항상 그 인스턴스 자신의 사본을 만들거나 갱신합니다. 그러니 모두에게 같은 값이면 클래스 변수를, 객체마다 다르면 인스턴스 변수를 쓰세요. __str__과 __repr__
__str__은 print()와 f-string이 객체를 어떻게 보여줄지 결정합니다. __repr__은 콘솔이나 디버깅에서 보이는 개발자용 표시를 결정합니다. 항상 __repr__을 정의하세요. 디버그용 표시와 별도로 깔끔한 사용자용 화면이 필요할 때 __str__을 정의하세요.
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__만 정의되어 있으면 파이썬은 두 경우 모두 그것을 사용합니다.
__str__은 print()와 f-string이 보여주는 친절한 버전입니다. __repr__은 콘솔에서 보게 되는 개발자용 화면입니다. 항상 __repr__을 작성하세요. __str__을 깜빡해도 일할 준비가 되어 있는 쪽이 바로 이겁니다. __str__은 사용자용 텍스트가 다르게 읽혀야 할 때만 추가하세요. 비공개 관례
파이썬에는 진짜 비공개 변수가 없지만, 이름 앞에 밑줄 하나를 붙이는 것(_balance)은 "이것은 내부용이니 클래스 바깥에서 직접 사용하지 말라"는 신호를 보내는 관례입니다. 언어가 강제하는 것이 아니라 다른 개발자에게 전하는 의사소통입니다.
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으로 바꿉니다. 이는 좀처럼 필요하지 않습니다. 대부분의 코드에서는 밑줄 하나가 관례입니다.
_balance)는 "내부용이니 바깥에서는 건드리지 말라"는 합의된 신호입니다. 아무것도 여러분이 접근하는 것을 막지 않습니다. 이것은 다른 개발자들, 그리고 미래의 여러분에게 보내는 메시지입니다. 밑줄 두 개는 서브클래스에서 이름 충돌을 피하기 위한 더 드문 도구이고, 일상적으로는 밑줄 하나를 쓰게 될 것입니다. 상속
클래스는 다른 클래스로부터 상속받아, 그 클래스의 모든 속성과 메서드를 자동으로 얻을 수 있습니다. 이후 서브클래스에서 특정 메서드를 오버라이드해 동작을 바꿀 수 있습니다. 이를 통해 공통된 기반을 재사용하고 필요한 곳만 특화할 수 있습니다.
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())Dog와 Cat은 Animal로부터 __init__을 상속받으므로, 자기만의 __init__이 필요 없습니다. 이들은 speak()만 자신의 동작으로 오버라이드합니다.
Dog와 Cat은 Animal의 __init__을 재사용하고 speak()만 다시 정의합니다. super()
super()는 부모 클래스의 메서드를 호출합니다. 부모의 동작을 완전히 대체하는 것이 아니라 확장하고 싶을 때 사용하세요. 부모의 __init__을 호출해 그 초기화를 실행한 다음, 서브클래스에 필요한 것을 추가로 더하는 식입니다.
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__()을 호출하세요.
super()는 부모 클래스에 접근합니다. 그래서 super().__init__()은 여러분이 자신의 것을 추가하기 전에 부모의 초기화를 실행합니다. 서브클래스가 자기만의 __init__을 작성하고 부모도 하나 가지고 있을 때 이것을 사용하세요. 빼먹으면 부모의 초기화가 실행되지 않아 객체가 절반만 만들어진 채로 남습니다. 클래스 메서드와 정적 메서드
@classmethod는 인스턴스 대신 클래스 자체를 전달받는 메서드를 만듭니다. 대안 생성자, 즉 문자열이나 파일, 다른 형식으로부터 인스턴스를 만들 때 유용합니다. @staticmethod는 정리 목적으로 클래스 안에 놓인 평범한 함수입니다. 인스턴스도 클래스도 전달받지 않습니다.
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")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를 사용하세요.
@classmethod는 인스턴스 대신 클래스를 전달받으므로, 문자열이나 파일 등 어떤 형식으로부터든 Player를 만드는 대안 생성자에 딱 맞습니다. @staticmethod는 정리를 위해 클래스 안에 넣어둔 평범한 함수이며, 클래스도 인스턴스도 받지 않습니다. 일반 메서드는 하나의 객체 데이터를 다루지만, 이 둘은 그렇지 않습니다. @property
@property를 사용하면 메서드를 속성처럼 접근할 수 있어서 괄호가 필요 없습니다. 다른 속성으로부터 계산되는 값이고, 단순한 속성 접근처럼 자연스럽게 읽히는 것에 사용하세요.
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는 메서드를 괄호 없이 속성처럼 읽게 해줍니다. c.area() 대신 c.area인 것이죠. 다른 속성으로부터 계산되고, 그냥 데이터처럼 읽히는 것이 자연스러운 값에 잘 맞습니다. 뒤에서는 여전히 접근할 때마다 여러분의 메서드가 실행됩니다. 실전 활용
인스턴스 속성, 메서드, @property, __str__을 가진 Player 클래스입니다:
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
