파이썬 객체지향 프로그래밍 심층 가이드

클래스 속성 타입 힌트

from typing import List, Dict, Optional

class Sample:
    def __init__(self, value_a: int, value_b: str, items: List[int], mapping: Dict[str, str], opt_value: Optional[float] = None):
        self.val_a = value_a
        self.val_b = value_b
        self.items = items
        self.mapping = mapping
        self.opt_val = opt_value

    def display(self) -> None:
        print(self.mapping)

sample = Sample(10, "hello", [1, 2], {'name': 'test'})
sample.display()

메서드 유형

파이썬에서 메서드는 데코레이터에 따라 호출 방식과 첫 번째 매개변수의 의미가 달라집니다.

특성 인스턴스 메서드 클래스 메서드 정적 메서드
기본 매개변수 self (인스턴스) cls (클래스) 없음
접근 가능 인스턴스 및 클래스 속성 클래스 속성만 클래스/인스턴스 속성 직접 접근 불가
주 용도 인스턴스 데이터 처리 팩토리 메서드, 클래스 상태 조작 클래스와 관련된 독립적인 유틸리티
class Human:
    category = "Homo sapiens"

    def __init__(self, name):
        self.name = name

    def greet(self):
        print(f"저는 {self.name}입니다.")

    @classmethod
    def get_category(cls):
        print(f"종은 {cls.category}입니다.")

    @staticmethod
    def is_adult(age):
        return age >= 18


p = Human("Alice")
p.greet()
Human.get_category()
print(Human.is_adult(20))

프로퍼티와 데이터 검증

@property 데코레이터를 사용하면 메서드를 속성처럼 호출할 수 있어 데이터 검증이나 계산된 필드에 유용합니다.

class PageController:
    def __init__(self, current_page: str, total_items: int, per_page=10):
        self.per_page = per_page
        self.total = total_items

        if not current_page.isdecimal():
            self.current = 1
        else:
            page_num = int(current_page)
            self.current = max(1, page_num)

    @property
    def start_index(self):
        return (self.current - 1) * self.per_page

    @property
    def end_index(self):
        return self.current * self.per_page


all_users = [f"User-{i}" for i in range(1, 3000)]
ctrl = PageController("2", len(all_users), 20)
page_data = all_users[ctrl.start_index:ctrl.end_index]
print(page_data[:3])

접근 제어 (Public & Private)

파이썬에서는 이름 앞에 __ (더블 언더스코어)를 붙여서 비공개(private) 멤버를 만듭니다. 이는 완전한 차단이 아닌 이름 변환(name mangling)을 통한 우회적 접근 제한입니다.

특징 1: 직접 접근 불가

class SampleClass:
    @property
    def __hidden(self):
        print("비공개 getter")

    @property
    def proxy_prop(self):
        print("공개 프록시")
        self.__hidden
        return 1


obj = SampleClass()
v = obj.proxy_prop
print(v)
# obj.__hidden()  # 오류 발생

특징 2: 이름 변환을 통한 접근

class Parent:
    def __hidden(self):
        print("부모의 비공개 메서드")

    def public_method(self):
        print("부모의 공개 메서드")

    def call_hidden(self):
        self.__hidden()  # 내부 호출은 가능


class Child(Parent):
    def test(self):
        self.public_method()
        # self.__hidden()  # 오류: 자식에서 부모의 비공개 메서드 호출 불가


obj = Parent()
# obj.__hidden()  # 오류
obj._Parent__hidden()  # 가능하지만 권장되지 않음

메타클래스

클래스 생성 방식을 제어합니다. 기본적으로 type 메타클래스를 사용합니다.

기본 클래스 생성

# 방법 1: class 키워드
class MyClass:
    attr1 = "value1"

    def method1(self):
        print("실행")
        return 123

    def method2(self):
        return lambda self: 4444


# 방법 2: type() 직접 호출
def my_function():
    print("실행")
    return 123

MyClassV2 = type("MyClassV2", (object,), {"attr1": "value1", "method1": my_function, "method2": lambda self: 4444})

커스텀 메타클래스

class CustomMeta(type):
    def __new__(cls, *args, **kwargs):
        # args[2]에 새 속성 추가
        modified_attrs = dict(args[2])
        modified_attrs["_added_by_meta"] = 1
        print(f"메타클래스가 클래스 생성: 이름={args[0]}, 베이스={args[1]}, 속성={args[2]}")
        new_class = super().__new__(cls, args[0], args[1], modified_attrs)
        return new_class


class Base(metaclass=CustomMeta):
    age = 25


class Sub(Base):
    pass

객체 관계와 포함

객체는 서로를 포함하거나 참조할 수 있습니다.

시나리오 1: 리스트 포함

class Member:
    def __init__(self, name, years):
        self.name = name
        self.years = years

    def info(self):
        msg = "회원 이름: {}, 나이: {}".format(self.name, self.years)
        print(msg)

m1 = Member("Kim", 25)
m2 = Member("Lee", 30)
m3 = Member("Park", 22)


class Group:
    def __init__(self, name):
        self.name = name
        self.members = []

    def add_member(self, member):
        self.members.append(member)

    def add_multiple(self, member_list):
        for m in member_list:
            self.add_member(m)

    def show_all(self):
        for m in self.members:
            m.info()

g = Group("Alpha Team")
g.add_member(m1)
g.add_multiple([m2, m3])
print(g.name)

시나리오 2: 객체 직접 참조

class StudentV2:
    def __init__(self, name, years, group):
        self.name = name
        self.years = years
        self.group = group

    def details(self):
        msg = "{}반 학생 {}, 나이 {}".format(self.group.title, self.name, self.years)
        print(msg)


class Course:
    def __init__(self, title):
        self.title = title


course1 = Course("Python 고급")
course2 = Course("데이터 과학")

students = [
    StudentV2("Alice", 22, course1),
    StudentV2("Bob", 23, course1),
    StudentV2("Charlie", 21, course2)
]

for s in students:
    print(s.name, s.group.title)

시나리오 3: 다중 참조

class StudentV3:
    def __init__(self, name, years, group):
        self.name = name
        self.years = years
        self.group = group


class CourseV2:
    def __init__(self, title, campus):
        self.title = title
        self.campus = campus


class Campus:
    def __init__(self, name):
        self.name = name


s1 = Campus("서울")
s2 = Campus("부산")

c1 = CourseV2("풀스택", s1)
c2 = CourseV2("클라우드", s2)

members = [
    StudentV3("Kim", 27, c1),
    StudentV3("Park", 28, c1),
    StudentV3("Choi", 26, c2)
]

for m in members:
    print(m.name, m.group.title, m.group.campus.name)

특수 메서드 심층 분석

__str__ vs __repr__

  • __str__ : print() 또는 str() 호출 시 사용. 사용자 가독성 목적.
  • __repr__ : 대화형 인터프리터에서 객체 출력 시, 또는 repr() 호출 시 사용. 개발자 정보 제공 목적.
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"사용자: {self.name} ({self.age}세)"

    def __repr__(self):
        return f"User(name='{self.name}', age={self.age})"

u = User("Alex", 30)
print(u)
print(repr(u))
print([u])

__call__ : 객체를 함수처럼 호출

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x, y=None):
        if y is None:
            return x * self.factor
        return (x + y) * self.factor

m = Multiplier(10)
print(m(5))
print(m(2, 3))
print(callable(m))

__new____init__

  • __new__ : 인스턴스 생성 (정적 메서드, 인스턴스 반환 필수)
  • __init__ : 생성된 인스턴스 초기화
import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __init__(self, name):
        self.name = name

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            with cls._lock:
                if not cls._instance:
                    print("새 인스턴스 생성...")
                    cls._instance = super().__new__(cls)
        return cls._instance

s1 = Singleton("A")
s2 = Singleton("B")
print(s1 is s2)
print(s1.name)

__dict____doc__

class Instructor:
    """강사 클래스"""
    role = "강사"

    def __init__(self, name):
        self.name = name

t = Instructor("Kim")
print(t.__dict__)
print(Instructor.__doc__)

시퀀스/컨테이너 메서드 (__getitem__, __setitem__, __delitem__)

class DataStore:
    def __init__(self):
        self._data = {}

    def __getitem__(self, key):
        return self._data.get(key, "키 없음")

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        if key in self._data:
            del self._data[key]

ds = DataStore()
ds["name"] = "Alice"
print(ds["name"])
del ds["name"]

속성 가로채기 (__getattr__, __setattr__, __delattr__)

class UserProxy:
    def __init__(self, name):
        # __setattr__ 트리거 주의: object.__setattr__ 사용 권장
        object.__setattr__(self, 'name', name)

    def __getattr__(self, item):
        return f"속성 '{item}'이 존재하지 않습니다."

    def __setattr__(self, key, value):
        print(f"할당 가로챔: {key} = {value}")
        object.__setattr__(self, key, value)

    def __delattr__(self, item):
        print(f"삭제 가로챔: {item}")
        object.__delattr__(self, item)

u = UserProxy("Bob")
print(u.name)
print(u.age)
u.age = 30
del u.age

연산자 오버로딩 (__add__)

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)

컨텍스트 매니저 (__enter__, __exit__)

class DBConnection:
    def __init__(self, db_name):
        self.db_name = db_name
        self.conn = None

    def __enter__(self):
        print(f"데이터베이스 연결 중: {self.db_name}...")
        self.conn = f"Conn_{self.db_name}"
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("데이터베이스 연결 종료...")
        self.conn = None
        if exc_type:
            print(f"예외 발생: {exc_val}")
        return False

with DBConnection("PostgreSQL") as conn:
    print(f"사용 중인 연결: {conn}")

추상 기본 클래스 (ABC)

추상 기본 클래스는 하위 클래스가 특정 메서드를 구현하도록 강제합니다. abc 모듈을 사용합니다.

import abc

class Shape(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def area(self):
        pass

    @abc.abstractmethod
    def perimeter(self):
        pass

    def description(self):
        return "도형"


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

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

    def perimeter(self):
        return 2 * 3.14 * self.radius


c = Circle(5)
print(c.area())

속성 및 메서드 숨기기

앞서 언급한 대로 __는 이름을 _ClassName__attribute 형식으로 변환합니다.

class Person:
    __type = "인간"
    home = "지구"

    def __init__(self, name):
        self.name = name

    def __hobby(self):
        print(f"{self.name}은/는 자유를 좋아합니다")

    def show_hobby(self):
        self.__hobby()


class Korean(Person):
    __type = "한국인"
    def __init__(self, name, home):
        super().__init__(name)
        self.home = home


p = Person("Kim")
# p.__hobby()  # 오류
# print(p.__type)  # 오류
print(p._Person__type)  # 가능
p.show_hobby()

k = Korean("Lee", "서울")
print(k._Person__type)  # 부모 클래스의 __type
print(k._Korean__type)  # 자식 클래스의 __type

리플렉션 (Reflection)

리플렉션을 사용하면 문자열 기반으로 객체의 속성과 메서드에 동적으로 접근할 수 있습니다.

내장 함수

  • getattr(obj, name)
  • setattr(obj, name, value)
  • hasattr(obj, name)
  • delattr(obj, name)
class Data:
    def __init__(self):
        self.value = 42
        self.label = "original"

d = Data()
print(getattr(d, 'value'))
setattr(d, 'label', 'modified')
print(d.label)
print(hasattr(d, 'missing'))
delattr(d, 'label')

import_module + 리플렉션

from importlib import import_module

# 모듈 동적 임포트
rand_module = import_module("random")
random_value = rand_module.randint(1, 100)

# requests.exceptions 모듈 임포트
req_exceptions = import_module("requests.exceptions")
invalid_url_class = getattr(req_exceptions, "InvalidURL")

# 패키지 내부 모듈
# mod = import_module("b.c.c")
# sub_mod = import_module(".c.c", package="b")

메서드 결정 순서 (MRO) - C3 선형화

다이아몬드 상속 문제를 해결하기 위해 C3 선형화 알고리즘을 사용합니다.

기본 예시

class D:
    pass

class C(D):
    pass

class B:
    pass

class A(B, C):
    pass

print(A.mro())
# [A, B, C, D, object]

복잡한 예시

# 가상의 복잡한 상속 구조
class H:
    pass

class G(H):
    pass

class F(G):
    pass

class E(F):
    pass

class D(E):
    pass

class C(D):
    pass

class B:
    pass

class A(B, C):
    pass

print(A.mro())

MRO 규칙: 왼쪽에서 오른쪽, 깊이 우선, 작은 다이아몬드는 위쪽 유지, 큰 다이아몬드는 위쪽 유지 (C3 알고리즘)

예외 처리

기본 구조

try:
    # 실행 코드
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"0으로 나누기 오류: {e}")
except Exception as e:
    print(f"일반 오류: {e}")
finally:
    print("항상 실행 (리소스 정리 등)")

커스텀 예외

class ValidationError(Exception):
    def __init__(self, message, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.message = message

def validate_email(email):
    if "@" not in email:
        raise ValidationError("유효하지 않은 이메일 형식")

try:
    validate_email("invalid")
except ValidationError as e:
    print(f"검증 오류: {e.message}")
except Exception as e:
    print(f"기타 오류: {e}")

finallyreturn

def test():
    try:
        print("try 블록")
        return "try 반환"
    except Exception:
        return "except 반환"
    finally:
        print("finally 블록 (return 이후에도 실행됨)")

result = test()
print(f"결과: {result}")

내장 함수 보충

callable()

def func():
    pass

class WithCall:
    def __call__(self):
        pass

class WithoutCall:
    pass

print(callable(func))
print(callable(WithCall))
print(callable(WithCall()))
print(callable(WithoutCall()))

super()

class Parent:
    def action(self, num):
        print("Parent.action", num)

class Child(Parent):
    def action(self, num):
        print("Child.action", num)
        super().action(num + 50)

c = Child()
c.action(10)

isinstance()issubclass()

class Animal:
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

d = Dog()
print(isinstance(d, Dog))
print(isinstance(d, Animal))
print(issubclass(Dog, Animal))
print(issubclass(Cat, Animal))

실전 예제: 간단한 TCP 서버

import socket
import threading

class BaseHandler:
    def __init__(self, request, client_addr, server):
        self.request = request
        self.client_addr = client_addr
        self.server = server
        self.setup()

    def __call__(self):
        try:
            self.handle()
        finally:
            self.finish()

    def setup(self):
        pass

    def handle(self):
        pass

    def finish(self):
        pass


class EchoHandler(BaseHandler):
    def handle(self):
        data = self.request.recv(1024)
        print(f"수신: {data}")
        self.request.sendall(b"Echo: " + data)


class ThreadedTCPServer:
    def __init__(self, addr, handler_class):
        self.addr = addr
        self.handler_class = handler_class
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(addr)
        self.sock.listen(5)

    def serve_forever(self):
        while True:
            client, addr = self.sock.accept()
            t = threading.Thread(target=self._process, args=(client, addr))
            t.start()

    def _process(self, request, addr):
        handler = self.handler_class(request, addr, self)
        handler()


if __name__ == "__main__":
    server = ThreadedTCPServer(("127.0.0.1", 8888), EchoHandler)
    server.serve_forever()

태그: 파이썬 객체지향 클래스 메타클래스 프로퍼티

7월 12일 21:12에 게시됨