Python 객체지향과 모듈 구조 (4‐5장): 클래스와 패키지 - glasslego/getting-started-with-python GitHub Wiki

4. 클래스와 객체지향

4.1 클래스 기초

4.1.1 클래스 정의와 객체 생성

# 기본 클래스 정의
class Person:
    # 클래스 변수 (모든 인스턴스가 공유)
    species = "Homo sapiens"
    
    # 생성자 메서드
    def __init__(self, name, age):
        # 인스턴스 변수
        self.name = name
        self.age = age
    
    # 인스턴스 메서드
    def introduce(self):
        return f"안녕하세요, 저는 {self.name}이고 {self.age}세입니다."
    
    def have_birthday(self):
        self.age += 1
        print(f"{self.name}의 나이가 {self.age}세가 되었습니다.")

# 객체 생성
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

# 메서드 호출
print(person1.introduce())  # "안녕하세요, 저는 Alice이고 25세입니다."
person1.have_birthday()     # "Alice의 나이가 26세가 되었습니다."

# 속성 접근
print(person1.name)         # "Alice"
print(person1.age)          # 26
print(Person.species)       # "Homo sapiens"

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

class MathUtils:
    pi = 3.14159
    
    def __init__(self, name):
        self.name = name
    
    # 인스턴스 메서드
    def instance_method(self):
        return f"{self.name}에서 호출됨"
    
    # 클래스 메서드
    @classmethod
    def circle_area(cls, radius):
        return cls.pi * radius ** 2
    
    # 정적 메서드
    @staticmethod
    def add(a, b):
        return a + b

# 사용 예시
math_obj = MathUtils("계산기")

# 인스턴스 메서드 (객체를 통해 호출)
print(math_obj.instance_method())

# 클래스 메서드 (클래스를 통해 호출)
print(MathUtils.circle_area(5))  # 78.53975

# 정적 메서드 (클래스나 객체 모두를 통해 호출 가능)
print(MathUtils.add(3, 4))       # 7
print(math_obj.add(3, 4))        # 7

4.2 상속

4.2.1 기본 상속

# 부모 클래스
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass  # 추상 메서드 (자식 클래스에서 구현)
    
    def info(self):
        return f"이름: {self.name}"

# 자식 클래스
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # 부모 클래스 생성자 호출
        self.breed = breed
    
    def speak(self):  # 메서드 오버라이딩
        return "멍멍!"
    
    def fetch(self):  # 자식 클래스만의 메서드
        return f"{self.name}이 공을 가져왔습니다."

class Cat(Animal):
    def speak(self):
        return "야옹!"
    
    def climb(self):
        return f"{self.name}이 나무에 올라갔습니다."

# 사용 예시
dog = Dog("바둑이", "진돗개")
cat = Cat("나비")

print(dog.info())    # "이름: 바둑이"
print(dog.speak())   # "멍멍!"
print(dog.fetch())   # "바둑이이 공을 가져왔습니다."

print(cat.info())    # "이름: 나비"
print(cat.speak())   # "야옹!"
print(cat.climb())   # "나비이 나무에 올라갔습니다."

# 다형성
animals = [dog, cat]
for animal in animals:
    print(f"{animal.name}: {animal.speak()}")

4.2.2 다중 상속

class Flyable:
    def fly(self):
        return "날고 있습니다."

class Swimmable:
    def swim(self):
        return "수영하고 있습니다."

class Duck(Animal, Flyable, Swimmable):
    def speak(self):
        return "꽥꽥!"

duck = Duck("도날드")
print(duck.speak())  # "꽥꽥!"
print(duck.fly())    # "날고 있습니다."
print(duck.swim())   # "수영하고 있습니다."

# MRO (Method Resolution Order) 확인
print(Duck.__mro__)  # 메서드 검색 순서

4.3 캡슐화

4.3.1 접근 제어자

class BankAccount:
    def __init__(self, owner, initial_balance=0):
        self.owner = owner              # public
        self._balance = initial_balance # protected (관례상 비공개)
        self.__account_number = self._generate_account_number()  # private
    
    def _generate_account_number(self):  # protected 메서드
        import random
        return f"ACC{random.randint(100000, 999999)}"
    
    def __validate_amount(self, amount):  # private 메서드
        return amount > 0
    
    # 잔액 조회 (getter)
    def get_balance(self):
        return self._balance
    
    # 입금
    def deposit(self, amount):
        if self.__validate_amount(amount):
            self._balance += amount
            return True
        return False
    
    # 출금
    def withdraw(self, amount):
        if self.__validate_amount(amount) and amount <= self._balance:
            self._balance -= amount
            return True
        return False
    
    def get_account_info(self):
        return f"계좌주: {self.owner}, 계좌번호: {self.__account_number}"

# 사용 예시
account = BankAccount("Alice", 1000)

print(account.owner)           # "Alice" (public 접근 가능)
print(account.get_balance())   # 1000

account.deposit(500)
print(account.get_balance())   # 1500

# print(account.__account_number)  # AttributeError (private 접근 불가)
# 하지만 name mangling으로 접근 가능 (권장하지 않음)
print(account._BankAccount__account_number)  # 실제 계좌번호

4.3.2 프로퍼티 (Property)

class Temperature:
    def __init__(self):
        self._celsius = 0
    
    # getter
    @property
    def celsius(self):
        return self._celsius
    
    # setter
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("절대영도보다 낮을 수 없습니다.")
        self._celsius = value
    
    # 계산된 속성
    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9
    
    @property
    def kelvin(self):
        return self._celsius + 273.15

# 사용 예시
temp = Temperature()
temp.celsius = 25
print(f"섭씨: {temp.celsius}°C")      # 25°C
print(f"화씨: {temp.fahrenheit}°F")   # 77.0°F
print(f"켈빈: {temp.kelvin}K")        # 298.15K

temp.fahrenheit = 100
print(f"섭씨: {temp.celsius}°C")      # 37.77777777777778°C

4.4 특수 메서드 (매직 메서드)

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # 문자열 표현
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    # 연산자 오버로딩
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    # 비교 연산자
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __lt__(self, other):
        return self.magnitude() < other.magnitude()
    
    # 길이 반환
    def __len__(self):
        return int(self.magnitude())
    
    # 인덱싱 지원
    def __getitem__(self, index):
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError("벡터는 0, 1 인덱스만 지원합니다.")
    
    def magnitude(self):
        return (self.x**2 + self.y**2)**0.5

# 사용 예시
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1)           # Vector(3, 4)
print(v1 + v2)      # Vector(4, 6)
print(v1 - v2)      # Vector(2, 2)
print(v1 * 2)       # Vector(6, 8)
print(v1 == v2)     # False
print(len(v1))      # 5
print(v1[0], v1[1]) # 3 4

5. 모듈과 패키지

5.1 모듈

5.1.1 모듈 생성과 import

# math_utils.py 파일 생성
def add(a, b):
    """두 수를 더합니다."""
    return a + b

def multiply(a, b):
    """두 수를 곱합니다."""
    return a * b

PI = 3.14159

class Calculator:
    def __init__(self):
        self.history = []
    
    def calculate(self, operation, a, b):
        if operation == "add":
            result = add(a, b)
        elif operation == "multiply":
            result = multiply(a, b)
        else:
            result = None
        
        self.history.append(f"{operation}({a}, {b}) = {result}")
        return result

# 실행될 때만 동작하는 코드
if __name__ == "__main__":
    print("math_utils 모듈이 직접 실행되었습니다.")
    print(f"PI 값: {PI}")
# main.py 파일에서 모듈 사용

# 1. 전체 모듈 import
import math_utils
result = math_utils.add(3, 4)
print(result)  # 7

# 2. 특정 함수만 import
from math_utils import add, PI
result = add(5, 6)
print(f"결과: {result}, PI: {PI}")

# 3. 별칭 사용
import math_utils as math
result = math.multiply(3, 4)

# 4. 모든 것을 import (권장하지 않음)
from math_utils import *
calc = Calculator()
print(calc.calculate("add", 10, 20))

5.1.2 내장 모듈 사용

# datetime 모듈
import datetime
from datetime import datetime, timedelta

now = datetime.now()
print(f"현재 시간: {now}")

tomorrow = now + timedelta(days=1)
print(f"내일: {tomorrow}")

# random 모듈
import random

print(random.randint(1, 10))      # 1~10 사이 정수
print(random.choice([1, 2, 3, 4, 5]))  # 리스트에서 랜덤 선택
print(random.random())            # 0~1 사이 실수

# os 모듈
import os

print(os.getcwd())                # 현재 작업 디렉토리
print(os.listdir('.'))            # 현재 디렉토리 파일 목록

# sys 모듈
import sys

print(sys.version)                # Python 버전
print(sys.path)                   # 모듈 검색 경로

# math 모듈
import math

print(math.sqrt(16))              # 4.0
print(math.pi)                    # 3.141592653589793
print(math.ceil(4.3))             # 5 (올림)
print(math.floor(4.7))            # 4 (내림)

# json 모듈
import json

data = {"name": "Alice", "age": 25}
json_string = json.dumps(data)    # 딕셔너리를 JSON 문자열로
print(json_string)                # {"name": "Alice", "age": 25}

parsed_data = json.loads(json_string)  # JSON 문자열을 딕셔너리로
print(parsed_data)                # {'name': 'Alice', 'age': 25}

5.2 패키지

5.2.1 패키지 구조 생성

my_package/
    __init__.py
    math_operations/
        __init__.py
        basic.py
        advanced.py
    string_operations/
        __init__.py
        formatting.py
        validation.py
# my_package/__init__.py
"""
수학과 문자열 연산을 위한 패키지
"""
__version__ = "1.0.0"
__author__ = "Your Name"

# 패키지 수준에서 import할 수 있는 것들
from .math_operations.basic import add, subtract
from .string_operations.formatting import capitalize_words

# my_package/math_operations/__init__.py
from .basic import add, subtract, multiply, divide
from .advanced import power, sqrt

# my_package/math_operations/basic.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def divide(a, b):
    if b == 0:
        raise ValueError("0으로 나눌 수 없습니다.")
    return a / b

# my_package/math_operations/advanced.py
import math

def power(base, exponent):
    return base ** exponent

def sqrt(number):
    return math.sqrt(number)

# my_package/string_operations/formatting.py
def capitalize_words(text):
    return ' '.join(word.capitalize() for word in text.split())

def remove_spaces(text):
    return text.replace(' ', '')
# 패키지 사용 예시

# 1. 전체 패키지 import
import my_package
result = my_package.add(3, 4)

# 2. 서브패키지 import
from my_package.math_operations import basic
result = basic.multiply(3, 4)

# 3. 특정 모듈 import
from my_package.math_operations.advanced import power
result = power(2, 3)

# 4. 여러 모듈에서 import
from my_package.math_operations import add, subtract
from my_package.string_operations.formatting import capitalize_words

print(add(10, 5))
print(capitalize_words("hello world"))

5.2.2 상대 import와 절대 import

# my_package/math_operations/basic.py 내에서

# 절대 import
from my_package.string_operations.formatting import capitalize_words

# 상대 import
from ..string_operations.formatting import capitalize_words  # 상위 패키지에서
from .advanced import power  # 같은 패키지에서

# my_package/math_operations/calculator.py
from .basic import add, subtract  # 같은 디렉토리
from ..string_operations import formatting  # 상위 디렉토리의 다른 패키지

class Calculator:
    def __init__(self):
        self.result = 0
    
    def add(self, value):
        self.result = add(self.result, value)
        return self
    
    def subtract(self, value):
        self.result = subtract(self.result, value)
        return self
    
    def get_result(self):
        return self.result

5.3 모듈과 패키지 고급 활용

5.3.1 동적 import

import importlib

# 문자열로 모듈 import
module_name = "math"
math_module = importlib.import_module(module_name)
print(math_module.sqrt(16))  # 4.0

# 조건부 import
try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
    print("NumPy가 설치되어 있지 않습니다.")

if HAS_NUMPY:
    # NumPy 기능 사용
    arr = np.array([1, 2, 3])
else:
    # 대안 구현
    arr = [1, 2, 3]

5.3.2 all 사용

# module_with_all.py
__all__ = ['public_function', 'PublicClass']

def public_function():
    """외부에서 사용할 수 있는 함수"""
    return "public"

def _private_function():
    """내부적으로만 사용하는 함수"""
    return "private"

class PublicClass:
    """외부에서 사용할 수 있는 클래스"""
    pass

class _PrivateClass:
    """내부적으로만 사용하는 클래스"""
    pass

# 사용할 때
from module_with_all import *
# public_function과 PublicClass만 import됨
# _private_function과 _PrivateClass는 import되지 않음

5.3.3 패키지 초기화

# my_package/__init__.py
import logging

# 패키지 로거 설정
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 패키지 전역 설정
_config = {
    'debug': False,
    'version': '1.0.0'
}

def get_config(key):
    """패키지 설정 값 조회"""
    return _config.get(key)

def set_config(key, value):
    """패키지 설정 값 변경"""
    _config[key] = value

# 패키지 import 시 실행되는 초기화 코드
print(f"my_package v{_config['version']} loaded")

# 편의를 위한 별칭
from .math_operations.basic import add as sum_numbers
from .string_operations.formatting import capitalize_words as title_case