Post

Python OOP Basic

Python OOP Basic

Python Essentials 2 과정의 핵심인 객체 지향 프로그래밍(OOP)을 기초부터 심화 응용까지 한 번에 정리했습니다. 기본 개념 설명 중간중간에 실제 시험 문제에 등장하는 까다로운 코드 예제들을 배치하여, 이론이 실제 코드에서 어떻게 동작하는지 깊이 있게 이해할 수 있도록 구성했습니다.

객체 지향 프로그래밍의 기초

절차적 접근 vs 객체 지향 접근

절차적 접근은 데이터와 코드가 분리되어 있지만, 객체 지향 접근(OOP)은 데이터와 코드를 객체(Object)라는 하나의 단위로 묶어 관리합니다. 이를 통해 대규모 프로젝트 관리와 데이터 보호(캡슐화)가 용이해집니다.

클래스와 객체

클래스(Class)는 설계도, 객체(Object)는 그 설계도로 만든 실체입니다.

1
2
3
4
5
class TheSimplestClass:
    pass

# 객체 생성 (인스턴스화)
my_object = TheSimplestClass()

스택(Stack) 구현으로 보는 OOP

데이터를 저장하는 방식인 스택(LIFO 구조)을 통해 클래스의 필요성을 알아봅니다.

1
2
3
4
5
6
7
8
9
10
11
class Stack:
    def __init__(self):
        self.__stack_list = []  # 비공개(Private) 변수

    def push(self, val):
        self.__stack_list.append(val)

    def pop(self):
        val = self.__stack_list[-1]
        del self.__stack_list[-1]
        return val

여기서 변수명 앞에 __를 붙이면 외부에서 접근할 수 없는 비공개 속성이 되어 데이터를 안전하게 보호할 수 있습니다.

클래스의 속성 (Properties)

인스턴스 변수와 클래스 변수의 차이

이 부분은 시험 문제에서 자주 혼동을 유발하는 영역입니다.

  • 인스턴스 변수: 객체마다 별도로 존재 (self.변수명)
  • 클래스 변수: 모든 객체가 공유 (클래스명.변수명)

다음 예제는 클래스 변수와 인스턴스 변수의 우선순위를 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
class A:
    v = 2  # 클래스 변수

class B(A):
    v = 1  # B 클래스에서 재정의된 클래스 변수

class C(B):
    pass

o = C()
print(o.v)

위 코드의 실행 결과는 1입니다. 파이썬은 속성을 찾을 때 1. 객체 내부(인스턴스 변수), 2. 객체의 클래스, 3. 부모 클래스 순서로 탐색합니다. C 클래스에는 v가 없으므로 부모인 B를 찾고, B에 v=1이 있으므로 더 상위인 A(v=2)까지 가지 않고 1을 출력합니다.

속성 값의 변경과 메서드

메서드를 통해 인스턴스 변수 값을 변경하고 반환하는 로직입니다.

1
2
3
4
5
6
7
8
9
10
class A:
    def __init__(self, v=1):
        self.v = v  # 인스턴스 변수 초기화

    def set(self, v):
        self.v = v
        return v

a = A() # a.v는 1로 초기화됨
print(a.set(a.v + 1)) # 1 + 1 = 2를 set에 전달

이 코드는 2를 출력합니다. 메서드 내부에서 self.v 값이 업데이트되고, 그 값이 반환되기 때문입니다.

메서드 (Methods)

생성자 (init)

객체가 생성될 때 자동으로 호출됩니다. 주로 속성 초기화에 사용되며 값을 반환할 수 없습니다.

문자열 표현 (str)

객체를 print() 함수로 출력하거나 문자열로 변환할 때 호출되는 메서드입니다. 반드시 문자열(string)을 반환해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
    def __str__(self):
        return 'a'

class B(A):
    def __str__(self):
        return 'b'

class C(B):
    pass

o = C()
print(o)

위 코드에서 C는 B를 상속받습니다. C에 __str__이 없으므로 부모인 B의 __str__이 실행되어 b가 출력됩니다. 만약 B에도 없었다면 A의 a가 출력되었을 것입니다.

이터레이터 (Iterator) 구현

클래스를 반복문(for)에서 사용하려면 __iter__와 next 메서드를 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class I:
    def __init__(self):
        self.s = 'abc'
        self.i = 0

    def __iter__(self):
        return self  # 관례적으로 iterator 객체 자신을 반환

    def __next__(self):
        if self.i == len(self.s):
            raise StopIteration  # 반복 종료 신호
        v = self.s[self.i]
        self.i += 1
        return v

for x in I():
    print(x, end='')

이 코드는 문자열 abc를 한 글자씩 순회하여 출력합니다. next 메서드는 호출될 때마다 다음 값을 반환하고, 끝에 도달하면 StopIteration 예외를 발생시켜야 합니다.

상속 (Inheritance)

상속은 코드 재사용의 핵심입니다.

상속 여부 확인 (issubclass)

두 클래스 간의 상속 관계를 확인할 수 있습니다.

1
2
3
4
5
class A: pass
class B(A): pass
class C(B): pass

print(issubclass(C, A))

결과는 True입니다. C는 B를 상속받고, B는 A를 상속받으므로 C는 A의 서브클래스입니다.

부모 클래스 생성자 호출

자식 클래스에서 부모의 __init__을 호출할 때는 두 가지 방법이 있습니다.

1
2
3
4
5
6
7
8
9
10
class A:
    def __init__(self):
        self.a = 1

class B(A):
    def __init__(self):
        # 방법 1: 부모 클래스 이름 명시 (self를 반드시 전달해야 함)
        A.__init__(self)  
        # 방법 2: super() 사용 (self 전달 불필요) -> super().__init__()
        self.b = 2

시험 문제에서는 A.init(self)와 같이 클래스 이름을 직접 사용하여 호출하는 형식이 자주 등장합니다.

다중 상속과 MRO (메서드 탐색 순서)

클래스가 여러 부모를 동시에 상속받을 때(다중 상속), 어떤 부모의 메서드를 먼저 사용할지 결정하는 순서가 MRO입니다. 기본적으로 왼쪽에서 오른쪽 순서입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A:
    def a(self):
        print('a')

class B:
    def a(self):
        print('b')

class C(B, A): # B를 먼저 상속받음
    def c(self):
        self.a()

o = C()
o.c()

이 코드의 결과는 b입니다. C(B, A)로 정의되었기 때문에, self.a()를 호출할 때 B 클래스의 a() 메서드를 먼저 발견하고 실행합니다. 만약 class C(A, B)였다면 a가 출력되었을 것입니다.

예외 처리의 확장 (Exception Handling)

단순한 에러 처리를 넘어 실행 흐름을 제어하는 고급 기법입니다.

try-except-else-finally 실행 흐름

이 구조는 에러 발생 여부에 따라 실행되는 블록이 달라집니다.

1
2
3
4
5
6
7
8
9
10
11
12
def f(x):
    try:
        x = x / x
    except:
        print("a", end='')  # 에러 발생 시
    else:
        print("b", end='')  # 에러 없을 시
    finally:
        print("c", end='')  # 무조건 실행

f(1) # 에러 없음 -> else(b), finally(c) 실행 -> 출력: bc
f(0) # ZeroDivisionError 발생 -> except(a), finally(c) 실행 -> 출력: ac

따라서 위 코드를 순차적으로 실행하면 전체 출력은 bcac가 됩니다.

사용자 정의 예외와 생성자 오버라이딩 주의점

Exception을 상속받을 때 부모 생성자 호출과 args 속성 관리에 주의해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
class Ex(Exception):
    def __init__(self, msg):
        # 부모에게는 msg를 두 번 반복해서 전달
        Exception.__init__(self, msg + msg)
        # 하지만 내 객체의 args 속성은 단일 msg로 덮어씀
        self.args = (msg,)

try:
    raise Ex('ex')
except Exception as e:
    print(e)

이 코드의 출력은 ex입니다. print(e)는 내부적으로 self.args를 참조하여 메시지를 출력하는데, 생성자 마지막 줄에서 self.args를 원본 msg(‘ex’)로 덮어썼기 때문입니다. 부모 생성자에 무엇을 넘겼는지보다, 최종적으로 self.args가 무엇인지가 중요합니다.

맺음말

이번 정리에서는 Python Essentials 2의 Module 3 내용을 기본 개념부터 시작하여, 실제 시험 문제에 등장하는 다중 상속, 실행 흐름 제어, 이터레이터 구현 등의 심화 내용까지 하나의 흐름으로 통합하였습니다. 객체 지향의 원리와 파이썬 내부 동작 방식을 이해하는데 도움이 되었기를 바랍니다.

This post is licensed under CC BY 4.0 by the author.