Python & AI Tutorials Logo
Python 프로그래밍

30. 클래스와 객체 소개

30.1) 객체 지향 프로그래밍 (나만의 타입 만들기)

이 책을 읽는 동안 여러분은 Python의 내장 타입: 정수, 문자열, 리스트, 딕셔너리 등과 함께 작업해 왔습니다. 각 타입은 데이터(예: 문자열의 문자들)와 그 데이터에 대해 수행할 수 있는 연산(예: .upper() 또는 .split())을 함께 묶어 둡니다. 이러한 데이터와 동작의 결합은 강력합니다. 문자열을 단순한 원시 문자 시퀀스가 아니라, 자체 기능을 가진 완전한 실체로 생각할 수 있게 해 주기 때문입니다.

객체 지향 프로그래밍(object-oriented programming, OOP)은 이 아이디어를 확장합니다. 문제 도메인에 특화된 데이터와 동작을 함께 묶는, 클래스(classes)라고 불리는 나만의 커스텀 타입을 만들 수 있게 해 줍니다. Python이 텍스트를 다루기 위한 str 타입과 시퀀스를 다루기 위한 list 타입을 제공하듯이, 금융 거래를 관리하기 위한 BankAccount 타입, 학업 기록을 추적하기 위한 Student 타입, 재고 시스템을 위한 Product 타입을 만들 수 있습니다.

왜 나만의 타입을 만들까요?

학교 시스템에서 학생 정보를 관리한다고 생각해 봅시다. 클래스 없이 하려면, 별도의 변수나 딕셔너리를 사용할 수 있습니다:

python
# 별도 변수를 사용하는 경우 - 금방 지저분해집니다
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
 
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
 
# 또는 딕셔너리를 사용하는 경우 - 더 낫지만, 여전히 제한이 있습니다
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}

이 접근은 단순한 경우에는 동작하지만, 한계가 있습니다:

  1. 검증(validation) 없음: gpa-5.0이나 "excellent" 같은 유효하지 않은 값으로 설정하는 것을 막아 주는 것이 없습니다
  2. 관련 동작(behavior) 없음: 우등 상태를 계산하거나 학생 정보를 포맷팅하는 같은 작업은 코드 전반에 흩어진 별도의 함수들로 존재합니다
  3. 타입 검사(type checking) 없음: 학생을 나타내는 딕셔너리는 다른 어떤 딕셔너리와도 똑같아 보입니다—Python은 학생 딕셔너리를 기대하는 곳에 실수로 상품 딕셔너리를 사용해 버리는 실수를 잡아주지 못합니다

클래스는 “학생이 정확히 무엇인지”와 “학생에 대해 어떤 연산이 의미 있는지”를 나타내는 새 타입을 정의할 수 있게 하여 이런 문제를 해결합니다:

python
# 여기까지 만들어 갈 것입니다 - 데이터와 동작을 묶는 Student 클래스
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def display_info(self):
        status = "Honors" if self.is_honors() else "Regular"
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
 
# 이제 student 객체를 만들 수 있습니다
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.display_info())  # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors())       # Output: True

이 장에서는 이런 클래스를 바닥부터 어떻게 만드는지 배웁니다. 가장 단순한 형태의 클래스부터 시작해 점진적으로 기능을 추가해, 풍부하고 유용한 커스텀 타입을 만들 수 있을 때까지 진행합니다.

클래스 vs 인스턴스: 설계도 비유

클래스(class)인스턴스(instance)의 차이를 이해하는 것은 객체 지향 프로그래밍의 기본입니다:

  • 클래스는 설계도(blueprint) 또는 템플릿(template)과 같습니다. 어떤 종류의 객체가 어떤 데이터를 담고 어떤 연산을 수행할 수 있는지를 정의합니다. 클래스 자체는 특정 학생이 아니라, “학생이란 무엇인가”에 대한 정의입니다.

  • 인스턴스(또는 객체(object)라고도 함)는 그 설계도에서 만들어진 구체적인 예시입니다. alice = Student("Alice Johnson", "S12345", 3.8)를 만들면, Alice의 구체적인 데이터를 가진 하나의 학생 인스턴스를 만드는 것입니다.

Student 클래스
설계도

alice 인스턴스
이름: Alice Johnson
ID: S12345
GPA: 3.8

bob 인스턴스
이름: Bob Smith
ID: S12346
GPA: 3.5

carol 인스턴스
이름: Carol Davis
ID: S12347
GPA: 3.9

한 개의 클래스에서 필요한 만큼 많은 인스턴스를 만들 수 있습니다. 이는 건축가가 하나의 설계도로 여러 채의 집을 지을 수 있는 것과 같습니다. 각 인스턴스는 자신만의 데이터(Alice의 GPA는 Bob의 GPA와 다름)를 가지지만, 클래스가 정의한 동일한 구조와 기능을 공유합니다.

이 장에서 배우게 될 내용

이 장에서는 Python 객체 지향 프로그래밍의 핵심 개념을 소개합니다:

  1. class 키워드로 클래스 정의하기
  2. 인스턴스 생성하기 및 그 속성(attributes)에 접근하기
  3. 인스턴스 데이터에서 동작하는 메서드(methods) 추가하기
  4. self 이해하기 및 메서드가 인스턴스 데이터에 접근하는 방법
  5. __init__ 메서드로 인스턴스 초기화하기
  6. __str____repr__문자열 표현 제어하기
  7. 같은 클래스에서 여러 개의 독립적인 인스턴스 만들기

이 장을 끝내면 프로그램을 더 체계적이고 유지보수 가능하며 표현력 있게 만들어 주는 커스텀 타입을 직접 설계하고 구현할 수 있게 됩니다. 31장에서는 더 고급 클래스 기능을, 32장에서는 상속(inheritance)과 다형성(polymorphism)을 다루며 이 기초를 확장해 나갈 것입니다.

30.2) class로 간단한 클래스 정의하기

먼저 가장 단순한 클래스—아직 어떤 데이터나 동작도 없는, 새 타입만 정의하는 클래스—를 만들어 봅시다.

class 키워드

클래스는 class 키워드 뒤에 클래스 이름과 콜론을 붙여 정의합니다:

python
class Student:
    pass  # 지금은 빈 클래스입니다
 
# 인스턴스 생성
alice = Student()
print(alice)  # Output: <__main__.Student object at 0x...>
print(type(alice))  # Output: <class '__main__.Student'>

이처럼 최소한의 클래스도 유용합니다—Student라는 새 타입을 만들기 때문입니다. alice = Student()로 인스턴스를 만들면, Python은 Student 타입의 새 객체를 생성합니다. 출력은 alice가 실제로 Student 객체임을 보여 주지만, 아직 흥미로운 일을 하지는 못합니다.

클래스 이름 규칙

Python의 클래스 이름은 CapWords 또는 PascalCase라는 특정 규칙을 따릅니다. 각 단어의 첫 글자를 대문자로 쓰고, 단어 사이에 밑줄을 쓰지 않습니다:

python
class BankAccount:      # Good: CapWords
    pass
 
class ProductInventory:  # Good: CapWords
    pass
 
class HTTPRequest:      # Good: Acronyms are all caps
    pass
 
# 클래스에는 이런 스타일을 피하세요:
# class bank_account:   # Wrong: snake_case is for functions/variables
# class bankaccount:    # Wrong: hard to read
# class BANKACCOUNT:    # Wrong: ALL_CAPS is for constants

이 규칙은 코드를 읽을 때 클래스와 함수/변수(이들은 snake_case를 사용함)를 구분하는 데 도움이 됩니다.

인스턴스 생성하기

클래스로부터 인스턴스를 만드는 것은 함수를 호출하는 것처럼 보입니다—클래스 이름 뒤에 괄호를 붙입니다:

python
class Product:
    pass
 
# 서로 다른 세 개의 product 인스턴스 생성
item1 = Product()
item2 = Product()
item3 = Product()
 
# 각 인스턴스는 서로 별개의 객체입니다
print(item1)  # Output: <__main__.Product object at 0x...>
print(item2)  # Output: <__main__.Product object at 0x...>
print(item3)  # Output: <__main__.Product object at 0x...>
 
# 같은 타입이지만 서로 다른 객체입니다
print(item1 is item2)  # Output: False
print(type(item1) is type(item2))  # Output: True

Product()를 호출할 때마다 새롭고 독립적인 인스턴스가 생성됩니다. 메모리 주소(0x... 부분)가 서로 다르므로, 메모리 안에서 분리된 객체임을 확인할 수 있습니다.

왜 빈 클래스부터 시작할까요?

아무것도 하지 않는 클래스로 시작하는 것이 의아할 수도 있습니다. 이유는 두 가지입니다:

  1. 개념적 명확성: 클래스는 데이터와 동작과는 별개로 “새 타입”이라는 점을 이해하면, 복잡성을 더하기 전에 기본 개념을 잡는 데 도움이 됩니다.

  2. 실용적 용도: 빈 클래스도 마커(marker)나 자리표시자(placeholder)로 유용할 수 있습니다. 예를 들어 커스텀 예외 타입을 정의할 수 있습니다:

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# 이 빈 클래스들은 서로 구분되는 오류 타입으로 동작합니다

하지만 실제 코드에서 빈 클래스는 드뭅니다. 이제 클래스를 유용하게 만들기 위해 데이터를 추가해 봅시다.

30.3) 인스턴스 생성과 속성 접근하기

클래스는 데이터를 담기 시작할 때 유용해집니다. Python에서는 단순히 대입하기만 하면 언제든지 속성(attributes-인스턴스에 붙어 있는 데이터)을 추가할 수 있습니다.

인스턴스에 속성 추가하기

점 표기법(dot notation)을 사용해 인스턴스에 속성을 추가할 수 있습니다:

python
class Student:
    pass
 
# 인스턴스 생성
alice = Student()
 
# 속성 추가
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
 
# 속성 접근
print(alice.name)        # Output: Alice Johnson
print(alice.student_id)  # Output: S12345
print(alice.gpa)         # Output: 3.8

점(.) 연산자는 속성에 접근합니다. alice.name은 “alice 객체의 name 속성을 가져라”라는 뜻입니다. 이 문법은 문자열(예: text.upper())이나 리스트(예: numbers.append(5))에서 이미 써 온 것과 같은 문법입니다—그것들도 해당 객체의 메서드와 속성에 접근하는 것입니다.

각 인스턴스는 자신만의 속성을 가집니다

같은 클래스의 서로 다른 인스턴스는 독립적인 속성을 가집니다:

python
class Student:
    pass
 
# 두 학생 생성
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# 각 인스턴스는 자신만의 데이터를 가집니다
print(alice.name)  # Output: Alice Johnson
print(bob.name)    # Output: Bob Smith
 
# 하나를 바꿔도 다른 하나에 영향이 없습니다
alice.gpa = 3.9
print(alice.gpa)  # Output: 3.9
print(bob.gpa)    # Output: 3.5 (unchanged)

이 독립성은 매우 중요합니다. alicebob은 각각 별도의 데이터를 가진 별개의 객체입니다. alice.gpa를 수정해도 bob.gpa에는 영향이 없습니다.

속성은 어떤 타입이든 될 수 있습니다

속성은 단순한 타입에만 제한되지 않고, 어떤 Python 값이든 담을 수 있습니다:

python
class Student:
    pass
 
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90]  # 리스트 속성
student.contact = {                 # 딕셔너리 속성
    "email": "carol@example.com",
    "phone": "555-0123"
}
student.is_active = True            # 불리언 속성
 
# 중첩된 데이터 접근
print(student.grades[0])           # Output: 95
print(student.contact["email"])    # Output: carol@example.com

이 유연성 덕분에 풍부한 데이터 구조로 복잡한 현실 세계의 개체를 모델링할 수 있습니다.

존재하지 않는 속성에 접근하기

존재하지 않는 속성에 접근하려고 하면 AttributeError가 발생합니다:

python
class Student:
    pass
 
student = Student()
student.name = "David Lee"
 
print(student.name)  # Output: David Lee
# print(student.age)  # AttributeError: 'Student' object has no attribute 'age'

이 오류는 유용합니다. 속성이 존재할 거라고 기대했지만 실제로는 없는 상황에서 발생하는 오타와 논리 오류를 잡아 주기 때문입니다.

수동으로 속성을 할당하는 방식의 문제점

인스턴스를 만든 뒤 속성을 수동으로 추가하는 것도 가능하지만, 이 방식에는 심각한 단점이 있습니다:

python
class Student:
    pass
 
# 속성을 빠뜨리거나 철자를 잘못 쓰기 쉽습니다
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# gpa 설정을 깜빡했습니다!
 
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346"  # 오타: student 대신 stuent
bob.gpa = 3.5
 
# 이제 alice는 gpa가 없고, bob은 오타가 있습니다
# print(alice.gpa)  # AttributeError
# print(bob.student_id)  # AttributeError

이것은 실수하기 쉽고 번거롭습니다. 모든 인스턴스가 올바른 속성으로 시작하도록 보장할 방법이 필요합니다. 그때 사용하는 것이 __init__ 메서드이며, 30.5절에서 다루겠습니다. 하지만 그 전에 메서드—클래스에 속한 함수—부터 배워 봅시다.

30.4) 인스턴스 메서드 추가하기: self 이해하기

메서드(methods)는 클래스 내부에 정의되는 함수로, 인스턴스 데이터에서 동작합니다. 메서드는 클래스에 데이터뿐 아니라 동작을 부여합니다.

간단한 메서드 정의하기

Student 클래스에 메서드를 추가해 봅시다:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# 인스턴스를 만들고 속성 추가
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# 메서드 호출
alice.display_info()  # Output: Alice Johnson - GPA: 3.8

메서드 display_info는 일반 함수와 마찬가지로 def로 클래스 안에서 정의합니다. 핵심적인 차이는 첫 번째 매개변수인 self입니다.

self 이해하기

self 매개변수는 메서드가 작업 대상인 특정 인스턴스에 접근하는 방법입니다. alice.display_info()를 호출하면 Python은 자동으로 alice를 메서드의 첫 번째 인자로 전달합니다. 메서드 내부에서 selfalice를 가리키므로, self.namealice.name에 접근하고 self.gpaalice.gpa에 접근합니다.

내부적으로는 다음과 같은 일이 벌어집니다:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# 아래 두 호출은 동일합니다:
alice.display_info()           # 일반적인 방식
Student.display_info(alice)    # Python이 실제로 하는 방식
 
# 둘 다 출력: Alice Johnson - GPA: 3.8

alice.display_info()라고 쓰면 Python은 이를 Student.display_info(alice)로 바꿉니다. 인스턴스(alice)가 메서드 안에서 self 매개변수가 됩니다.

왜 "self"인가요?

self는 키워드가 아니라 관례(convention)로 정해진 이름입니다. 기술적으로는 어떤 이름이든 사용할 수 있습니다:

python
class Student:
    def display_info(this):  # 동작하지만, 이렇게 하지 마세요
        print(f"{this.name} - GPA: {this.gpa}")

하지만 항상 self를 사용하세요. 이는 모든 Python 코드에서 통용되는 관례이며, 다른 Python 프로그래머가 코드를 읽기 쉽게 해 줍니다. 다른 이름을 쓰면 독자를 혼란스럽게 하고 커뮤니티 표준을 어기게 됩니다.

여러 인스턴스에서의 메서드

여러 인스턴스가 있을 때 self의 힘이 분명해집니다:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# 두 학생 생성
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# 같은 메서드, 다른 데이터
alice.display_info()  # Output: Alice Johnson - GPA: 3.8
bob.display_info()    # Output: Bob Smith - GPA: 3.5

alice.display_info()를 호출하면 selfalice입니다. bob.display_info()를 호출하면 selfbob입니다. 같은 메서드 코드가 어떤 인스턴스에도 작동하는 이유는 self가 호출한 인스턴스에 맞춰 바뀌기 때문입니다.

alice.display_info

self = alice

bob.display_info

self = bob

alice.name
alice.gpa 접근

bob.name
bob.gpa 접근

메서드는 추가 매개변수를 받을 수 있습니다

메서드는 self 외에도 매개변수를 받을 수 있습니다:

python
class Student:
    def update_gpa(self, new_gpa):
        self.gpa = new_gpa
        print(f"Updated {self.name}'s GPA to {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
alice.update_gpa(3.9)  # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa)       # Output: 3.9

alice.update_gpa(3.9)를 호출하면 Python은 aliceself로, 3.9new_gpa로 전달합니다. 메서드 시그니처(signature)는 def update_gpa(self, new_gpa)이지만, 호출할 때는 인자를 하나만 전달합니다—self는 Python이 자동으로 처리합니다.

메서드는 값을 반환할 수 있습니다

메서드는 일반 함수처럼 값을 반환할 수 있습니다:

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def get_status(self):
        if self.is_honors():
            return "Honors Student"
        else:
            return "Regular Student"
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
 
print(alice.get_status())  # Output: Honors Student
print(bob.get_status())    # Output: Regular Student

get_statusself.is_honors()를 사용해 다른 메서드(is_honors)를 호출하는 점에 주목하세요. 메서드는 같은 인스턴스의 다른 메서드를 호출할 수 있습니다.

메서드 vs 함수: 언제 무엇을 쓸까요?

메서드를 쓸지, 독립적인 함수를 쓸지 고민될 수 있습니다. 기준은 다음과 같습니다:

연산이 다음에 해당하면 메서드를 사용하세요:

  • 인스턴스 데이터(self.name, self.gpa 등)에 접근해야 한다
  • 논리적으로 해당 타입에 속한다(학생이 하는 일 또는 성질이다)
  • 인스턴스의 상태(state)를 변경한다

연산이 다음에 해당하면 독립 함수(standalone function)를 사용하세요:

  • 인스턴스 데이터가 필요 없다
  • 여러 타입에 대해 동작한다
  • 범용 유틸리티이다
python
class Student:
    # 메서드: 인스턴스 데이터가 필요함
    def is_honors(self):
        return self.gpa >= 3.5
 
# 함수: 범용 유틸리티, 어떤 GPA 값에도 동작함
def calculate_letter_grade(gpa):
    if gpa >= 3.7:
        return "A"
    elif gpa >= 3.0:
        return "B"
    elif gpa >= 2.0:
        return "C"
    else:
        return "D"
 
alice = Student()
alice.gpa = 3.8
 
# 인스턴스 전용 검사는 메서드 사용
print(alice.is_honors())  # Output: True
 
# 일반 계산은 함수 사용
print(calculate_letter_grade(alice.gpa))  # Output: A
print(calculate_letter_grade(2.5))        # Output: C

흔한 메서드 패턴

자주 사용하게 될 몇 가지 패턴은 다음과 같습니다:

게터(getter) 메서드(계산된 정보 가져오기):

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

세터(setter) 메서드(검증과 함께 속성 변경):

python
class Student:
    def set_gpa(self, new_gpa):
        if 0.0 <= new_gpa <= 4.0:
            self.gpa = new_gpa
        else:
            print("Invalid GPA: must be between 0.0 and 4.0")

질의(query) 메서드(예/아니오 질문에 답하기):

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

동작(action) 메서드(작업 수행):

python
class Student:
    def add_grade(self, grade):
        self.grades.append(grade)
        # 모든 성적을 기반으로 GPA 재계산
        self.gpa = sum(self.grades) / len(self.grades)

30.5) __init__로 인스턴스 초기화하기

인스턴스를 만든 뒤 속성을 수동으로 설정하는 일은 번거롭고 실수하기 쉽습니다. __init__ 메서드는 인스턴스를 생성할 때 데이터를 함께 넘겨 초기화할 수 있게 해 줌으로써 이 문제를 해결합니다.

__init__ 메서드

__init__ 메서드(발음은 “dunder init” 또는 “init”)는 새 인스턴스를 만들 때 Python이 자동으로 호출하는 특별 메서드입니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# 초기 데이터를 넣어 인스턴스 생성
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.name)  # Output: Alice Johnson
print(bob.gpa)     # Output: 3.5

Student("Alice Johnson", "S12345", 3.8)라고 쓰면 Python은 다음을 수행합니다:

  1. 비어 있는 새 Student 인스턴스를 생성합니다
  2. 그 인스턴스를 self로, 여러분이 전달한 인자들을 함께 넘겨 __init__를 호출합니다
  3. 초기화된 인스턴스를 반환합니다

__init__ 메서드는 명시적으로 값을 반환하지 않습니다—인스턴스의 속성을 설정하여 인스턴스를 제자리에서 수정합니다. __init__에서 값을 반환하려고 하면 Python은 TypeError를 발생시킵니다.

python
class Student:
    def __init__(self, name):
        self.name = name
        # __init__에서는 아무것도 반환하지 마세요
        # return self  # 잘못됨! TypeError: __init__() should return None, not 'Student'

__init__이 동작하는 방식

무슨 일이 일어나는지 단계별로 살펴봅시다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        print(f"Initializing student: {name}")
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
        print(f"Initialization complete")
 
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
 
print(alice.name)  # Output: Alice Johnson

self 뒤의 매개변수(name, student_id, gpa)는 인스턴스를 생성할 때 필요한 필수 인자가 됩니다. 이를 제공하지 않으면 Python은 TypeError를 발생시킵니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# student = Student()  # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice")  # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8)  # Correct

이는 수동 속성 할당보다 훨씬 낫습니다—Python이 모든 인스턴스가 필수 데이터로 시작하도록 강제해 주기 때문입니다.

__init__의 기본 매개변수 값

일반 함수처럼 __init__에서도 기본값을 사용할 수 있습니다:

python
class Student:
    def __init__(self, name, student_id, gpa=0.0):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# GPA는 선택 사항이며, 기본값은 0.0입니다
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346")  # 기본값 gpa=0.0 사용
 
print(alice.gpa)  # Output: 3.8
print(bob.gpa)    # Output: 0.0

이는 합리적인 기본값이 있지만 필요 시 커스터마이즈할 수 있는 속성에 유용합니다.

__init__에서 검증하기

__init__에서 입력을 검증해 인스턴스가 유효한 상태로 시작하도록 할 수 있습니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        if not name:
            print("Error: Name cannot be empty")
            self.name = "Unknown"
        else:
            self.name = name
        
        self.student_id = student_id
        
        if 0.0 <= gpa <= 4.0:
            self.gpa = gpa
        else:
            print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
            self.gpa = 0.0
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa)  # Output: 3.8
 
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name)  # Output: Unknown
print(bob.gpa)   # Output: 0.0

이렇게 하면 누군가 유효하지 않은 데이터를 넘겨도, 인스턴스는 합리적인 상태로 남게 됩니다.

30.6) __str____repr__로 문자열 표현 만들기

print()함수로 인스턴스를 출력하거나 REPL에서 볼 때 Python은 이를 문자열로 변환해야 합니다. 기본값으로는 도움이 되지 않는 결과가 나옵니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: <__main__.Student object at 0x...>

기본 출력은 클래스 이름과 메모리 주소를 보여 주지만, Alice의 실제 데이터에 대한 정보는 없습니다. 이를 __str____repr__ 특수 메서드로 커스터마이즈할 수 있습니다.

__str__ 메서드

__str__ 메서드는 print()str()이 인스턴스를 문자열로 변환하는 방법을 정의합니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice))  # Output: Alice Johnson (S12345) - GPA: 3.8

__str__은 최종 사용자에게 읽기 쉽고 유용한 문자열을 반환해야 합니다. “친절한(friendly)” 표현이라고 생각하면 됩니다.

__repr__ 메서드

__repr__ 메서드는 REPL과 repr()에서 사용되는 인스턴스의 "공식" 문자열 표현을 정의합니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice))  # Output: Student('Alice Johnson', 'S12345', 3.8)

REPL에서는:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

__repr__은 객체를 다시 만들 수 있는 유효한 Python 코드처럼 보이는 문자열을 반환해야 합니다. “개발자(developer)” 표현이라고 생각하면 됩니다—모호하지 않고 디버깅에 유용해야 합니다.

__str____repr__을 함께 사용하기

서로 다른 목적을 위해 둘 다 정의할 수 있습니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        # 친절하고 읽기 쉬운 형식
        return f"{self.name} - GPA: {self.gpa}"
    
    def __repr__(self):
        # 모호하지 않고 코드처럼 보이는 형식
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
 
print(alice)        # __str__ 사용
# Output: Alice Johnson - GPA: 3.8
 
print(repr(alice))  # __repr__ 사용
# Output: Student('Alice Johnson', 'S12345', 3.8)

REPL에서는:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice  # __repr__ 사용
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice)  # __str__ 사용
Alice Johnson - GPA: 3.8

어떤 메서드를 언제 정의해야 할까요?

기준은 다음과 같습니다:

  • 항상 __repr__을 정의하세요: REPL과 디버깅 도구에서 사용됩니다. 하나만 정의한다면 이것을 정의하세요.
  • 최종 사용자용 출력 형식이 필요하면 __str__을 정의하세요: 사용자에게 출력될 클래스라면 읽기 쉬운 __str__을 제공하세요.
  • __repr__만 정의한 경우: Python은 repr()에 이를 사용하며, str()__repr__을 대체(fallback)로 사용합니다 (따라서 print()도 이를 사용합니다).
  • __str__만 정의한 경우: print()__str__을 사용하지만, repr()과 REPL은 기본 __repr__을 사용합니다 (메모리 주소 표시). 이것이 __repr__를 정의하는 것이 더 중요한 이유입니다.
python
# __repr__만 정의한 경우
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
 
item = Product("Laptop", 999.99)
print(item)        # 폴백으로 __repr__ 사용
# Output: Product('Laptop', 999.99)
print(repr(item))  # __repr__ 사용
# Output: Product('Laptop', 999.99)

컬렉션에서의 문자열 표현

인스턴스가 컬렉션(리스트, 딕셔너리 등) 내부에 있을 때, Python은 __str__이 아닌 __repr__을 사용하여 표시합니다:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name}: {self.gpa}"
    
    def __repr__(self):
        return f"Student('{self.name}', {self.gpa})"
 
students = [
    Student("Alice", 3.8),
    Student("Bob", 3.5),
    Student("Carol", 3.9)
]
 
# 리스트를 출력하면 각 student에 대해 __repr__을 사용합니다
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
 
# 개별 student를 출력하면 __str__을 사용합니다
for student in students:
    print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9

이것이 __repr__이 명확해야 하는 이유입니다—디버깅할 때 데이터 구조 안에 무엇이 있는지 파악하는 데 도움이 되기 때문입니다. 리스트를 출력하면 Python은 각 요소마다 repr()을 호출하여 구조를 명확하게 보여줍니다.

30.7) 여러 개의 독립적인 인스턴스 만들기

클래스의 가장 강력한 점 중 하나는, 각자 자신만의 데이터를 가진 독립적인 인스턴스를 많이 만들 수 있다는 것입니다. 이를 자세히 살펴봅시다.

각 인스턴스는 자신만의 데이터를 가집니다

같은 클래스에서 여러 인스턴스를 만들면, 각 인스턴스는 자신만의 별도 속성을 유지합니다:

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0.0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
            return True
        else:
            print(f"Insufficient funds. Balance: ${self.balance:.2f}")
            return False
    
    def __str__(self):
        return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
 
# 서로 독립적인 계정 3개 생성
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
 
# 한 계정에 대한 작업은 다른 계정에 영향을 주지 않습니다
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
 
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
 
# 각 계정은 자신만의 잔액을 유지합니다
print(alice_account)  # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account)    # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account)  # Output: Carol Davis's account (ACC-003): $2000.00

이 독립성은 객체 지향 프로그래밍의 기본입니다. 각 인스턴스는 자신만의 상태(state)를 가진 별개의 실체입니다.

컬렉션에서의 인스턴스

인스턴스는 리스트, 딕셔너리 또는 다른 어떤 컬렉션에도 저장할 수 있습니다:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
# 학생 리스트 생성
students = [
    Student("Alice Johnson", "S12345", 3.8),
    Student("Bob Smith", "S12346", 3.2),
    Student("Carol Davis", "S12347", 3.9),
    Student("David Lee", "S12348", 3.4)
]
 
# 우등 학생 모두 찾기
honors_students = []
for student in students:
    if student.is_honors():
        honors_students.append(student)
 
print("Honors students:")
for student in honors_students:
    print(f"  {student.name}: {student.gpa}")
# Output:
# Honors students:
#   Alice Johnson: 3.8
#   Carol Davis: 3.9
 
# 평균 GPA 계산
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}")  # Output: Average GPA: 3.58

이것은 흔한 패턴입니다. 여러 인스턴스를 만들고 컬렉션에 저장한 뒤, 반복문(loop)과 컴프리헨션(comprehension)으로 처리합니다.

인스턴스는 다른 인스턴스를 참조할 수 있습니다

인스턴스는 다른 인스턴스를 참조하는 속성을 가질 수 있어, 객체 간의 관계를 만들 수 있습니다:

python
class Course:
    def __init__(self, course_code, course_name):
        self.course_code = course_code
        self.course_name = course_name
    
    def __str__(self):
        return f"{self.course_code}: {self.course_name}"
 
class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.courses = []  # Course 인스턴스의 리스트
    
    def enroll(self, course):
        self.courses.append(course)
        print(f"{self.name} enrolled in {course.course_name}")
    
    def list_courses(self):
        print(f"{self.name}'s courses:")
        for course in self.courses:
            print(f"  {course}")
 
# 강좌 생성
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
 
# 학생 생성 후 강좌 수강 등록
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
 
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
 
# 각 학생의 강좌 목록 출력
alice.list_courses()
# Output:
# Alice Johnson's courses:
#   CS101: Introduction to Python
#   CS102: Data Structures
 
bob.list_courses()
# Output:
# Bob Smith's courses:
#   CS101: Introduction to Python
#   CS103: Web Development

Alice와 Bob이 모두 python_course에 등록되어 있다는 점에 주목하세요—둘은 같은 Course 인스턴스를 참조합니다. 이는 여러 학생이 같은 강좌를 들을 수 있는 현실 세계의 관계를 모델링합니다.

인스턴스의 정체성(identity)과 동등성(equality)

각 인스턴스는, 다른 인스턴스와 같은 데이터를 가지고 있더라도, 고유한 객체입니다:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
 
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
 
# 데이터가 같아도 서로 다른 객체입니다
print(alice1 is alice2)  # Output: False
print(id(alice1) == id(alice2))  # Output: False

기본적으로 ==도 동등한 데이터인지가 아니라 정체성(같은 객체인지)을 검사합니다. 31장에서는 __eq__ 특수 메서드로 동등성 비교를 커스터마이즈하는 방법을 배웁니다.


이 장에서는 Python 객체 지향 프로그래밍의 기초를 소개했습니다. 클래스를 정의하고, 인스턴스를 생성하고, 메서드를 추가하고, __init__로 인스턴스를 초기화하고, 문자열 표현을 제어하며, 여러 독립적인 인스턴스로 작업하는 방법을 배웠습니다. 이러한 개념은 31장과 32장에서 다룰 더 고급 OOP 기능의 기반이 됩니다.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai