객체지향 프로그래밍 기초
객체지향 프로그래밍 기초¶
참고: 이 레슨은 선수 지식 복습용입니다. 고급 레슨(데코레이터, 메타클래스 등)을 시작하기 전에 OOP 기초가 부족하다면 이 내용을 먼저 학습하세요.
학습 목표¶
- 클래스와 객체(인스턴스)의 개념 이해
- 생성자, 인스턴스/클래스 변수, 메서드 활용
- 상속, 다형성, 캡슐화 원리 파악
- 특수 메서드(매직 메서드) 기본 활용
1. 클래스와 객체¶
1.1 기본 개념¶
┌─────────────────────────────────────────────────────────────────┐
│ 객체지향 프로그래밍 (OOP) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 클래스 (Class): │
│ - 객체의 청사진(설계도) │
│ - 속성(변수)과 행동(메서드) 정의 │
│ │
│ 객체 (Object) / 인스턴스 (Instance): │
│ - 클래스로부터 생성된 실체 │
│ - 각 객체는 독립적인 상태(데이터)를 가짐 │
│ │
│ 예시: │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ class Dog │ ──────▶ │ my_dog │ # 객체 1 │
│ │ name │ │ name="Max" │ │
│ │ age │ │ age=3 │ │
│ │ bark() │ └─────────────┘ │
│ │ eat() │ │
│ └─────────────┘ ──────▶ ┌─────────────┐ │
│ │ your_dog │ # 객체 2 │
│ │ name="Bella"│ │
│ │ age=5 │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 클래스 정의¶
class Dog:
"""강아지 클래스"""
# 클래스 변수 (모든 인스턴스가 공유)
species = "Canis familiaris"
# 생성자 (초기화 메서드)
def __init__(self, name, age):
"""
Args:
name: 강아지 이름
age: 강아지 나이
"""
# 인스턴스 변수 (각 객체마다 독립)
self.name = name
self.age = age
# 인스턴스 메서드
def bark(self):
"""짖기"""
print(f"{self.name} says: Woof!")
def describe(self):
"""설명"""
return f"{self.name} is {self.age} years old"
def birthday(self):
"""생일"""
self.age += 1
print(f"Happy birthday, {self.name}! Now {self.age} years old.")
# 객체 생성
my_dog = Dog("Max", 3)
your_dog = Dog("Bella", 5)
# 속성 접근
print(my_dog.name) # Max
print(my_dog.species) # Canis familiaris
# 메서드 호출
my_dog.bark() # Max says: Woof!
print(my_dog.describe()) # Max is 3 years old
my_dog.birthday() # Happy birthday, Max! Now 4 years old.
# 클래스 변수 vs 인스턴스 변수
print(Dog.species) # 클래스로 접근
print(my_dog.species) # 인스턴스로 접근 (같은 값)
1.3 self의 의미¶
class Circle:
def __init__(self, radius):
# self = 현재 생성되는 인스턴스를 참조
self.radius = radius
def area(self):
# self.radius = 이 인스턴스의 radius
return 3.14159 * self.radius ** 2
def circumference(self):
return 2 * 3.14159 * self.radius
# 객체 생성 시
c1 = Circle(5) # self = c1
c2 = Circle(10) # self = c2
# 메서드 호출 시
print(c1.area()) # self = c1, self.radius = 5
print(c2.area()) # self = c2, self.radius = 10
# 실제로는 이렇게 동작:
# Circle.area(c1) → self = c1
2. 클래스/인스턴스 변수와 메서드¶
2.1 변수 종류¶
class Counter:
# 클래스 변수: 모든 인스턴스가 공유
total_count = 0
def __init__(self, name):
# 인스턴스 변수: 각 객체마다 독립
self.name = name
self.count = 0
# 클래스 변수 수정
Counter.total_count += 1
def increment(self):
self.count += 1
def get_count(self):
return self.count
c1 = Counter("Counter 1")
c2 = Counter("Counter 2")
c1.increment()
c1.increment()
c2.increment()
print(c1.count) # 2 (인스턴스 변수)
print(c2.count) # 1 (인스턴스 변수)
print(Counter.total_count) # 2 (클래스 변수)
# 주의: 인스턴스로 클래스 변수를 "할당"하면 인스턴스 변수가 됨
c1.total_count = 100 # c1만의 인스턴스 변수 생성
print(c1.total_count) # 100 (인스턴스)
print(Counter.total_count) # 2 (클래스 변수 그대로)
2.2 메서드 종류¶
class MyClass:
class_var = 0
def __init__(self, value):
self.instance_var = value
# 인스턴스 메서드: self 사용, 인스턴스 데이터 접근
def instance_method(self):
return f"Instance: {self.instance_var}"
# 클래스 메서드: cls 사용, 클래스 데이터 접근
@classmethod
def class_method(cls):
cls.class_var += 1
return f"Class var: {cls.class_var}"
# 정적 메서드: self도 cls도 없음, 독립적인 함수
@staticmethod
def static_method(x, y):
return x + y
obj = MyClass(42)
# 인스턴스 메서드
print(obj.instance_method()) # Instance: 42
# 클래스 메서드 (클래스나 인스턴스로 호출)
print(MyClass.class_method()) # Class var: 1
print(obj.class_method()) # Class var: 2
# 정적 메서드 (클래스나 인스턴스로 호출)
print(MyClass.static_method(3, 4)) # 7
print(obj.static_method(3, 4)) # 7
2.3 팩토리 메서드 패턴¶
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
# 팩토리 메서드: 다양한 형식에서 객체 생성
@classmethod
def from_string(cls, date_string):
"""'YYYY-MM-DD' 형식에서 생성"""
year, month, day = map(int, date_string.split("-"))
return cls(year, month, day)
@classmethod
def today(cls):
"""오늘 날짜로 생성"""
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
# 다양한 방법으로 객체 생성
d1 = Date(2024, 1, 15)
d2 = Date.from_string("2024-06-20")
d3 = Date.today()
print(d1) # Date(2024, 1, 15)
print(d2) # Date(2024, 6, 20)
print(d3) # Date(현재 날짜)
3. 상속 (Inheritance)¶
3.1 기본 상속¶
# 부모 클래스 (슈퍼클래스, 기반 클래스)
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement")
def describe(self):
return f"I am {self.name}"
# 자식 클래스 (서브클래스, 파생 클래스)
class Dog(Animal):
def __init__(self, name, breed):
# 부모 클래스 초기화
super().__init__(name)
self.breed = breed
def speak(self):
return f"{self.name} says Woof!"
def fetch(self):
return f"{self.name} is fetching the ball"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
def scratch(self):
return f"{self.name} is scratching"
# 사용
dog = Dog("Max", "Golden Retriever")
cat = Cat("Whiskers")
print(dog.describe()) # I am Max (부모 메서드)
print(dog.speak()) # Max says Woof! (오버라이드)
print(dog.fetch()) # Max is fetching the ball (자식 전용)
print(dog.breed) # Golden Retriever
print(cat.speak()) # Whiskers says Meow!
# 상속 관계 확인
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True
print(issubclass(Dog, Animal)) # True
3.2 메서드 오버라이딩¶
class Vehicle:
def __init__(self, brand):
self.brand = brand
def info(self):
return f"Brand: {self.brand}"
def move(self):
return "Moving..."
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand)
self.model = model
# 메서드 오버라이딩 (재정의)
def info(self):
# 부모 메서드 호출 + 확장
parent_info = super().info()
return f"{parent_info}, Model: {self.model}"
def move(self):
# 완전히 새로운 구현
return "Driving on the road"
def honk(self):
return "Beep beep!"
class Motorcycle(Vehicle):
def move(self):
return "Riding on two wheels"
car = Car("Toyota", "Camry")
print(car.info()) # Brand: Toyota, Model: Camry
print(car.move()) # Driving on the road
moto = Motorcycle("Harley")
print(moto.move()) # Riding on two wheels
3.3 다중 상속¶
class Flyable:
def fly(self):
return "Flying in the sky"
class Swimmable:
def swim(self):
return "Swimming in the water"
class Duck(Animal, Flyable, Swimmable):
def speak(self):
return f"{self.name} says Quack!"
duck = Duck("Donald")
print(duck.describe()) # I am Donald (Animal)
print(duck.fly()) # Flying in the sky (Flyable)
print(duck.swim()) # Swimming in the water (Swimmable)
print(duck.speak()) # Donald says Quack!
# MRO (Method Resolution Order) 확인
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Animal'>, <class 'Flyable'>, <class 'Swimmable'>, <class 'object'>)
4. 캡슐화 (Encapsulation)¶
4.1 접근 제어¶
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # public (공개)
self._balance = balance # protected (보호, 관례상)
self.__pin = "1234" # private (비공개, 네임 맹글링)
def deposit(self, amount):
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount, pin):
if pin != self.__pin:
raise ValueError("Invalid PIN")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
return amount
def get_balance(self):
return self._balance
account = BankAccount("Alice", 1000)
# public 접근
print(account.owner) # Alice
# protected 접근 (가능하지만 권장하지 않음)
print(account._balance) # 1000
# private 접근 (직접 불가)
# print(account.__pin) # AttributeError
# 네임 맹글링으로 접근 가능 (권장하지 않음)
print(account._BankAccount__pin) # 1234
# 메서드를 통한 안전한 접근
account.deposit(500)
print(account.get_balance()) # 1500
4.2 프로퍼티 (Property)¶
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
# getter
@property
def celsius(self):
return self._celsius
# setter
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
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
temp = Temperature()
# 프로퍼티 사용 (속성처럼)
temp.celsius = 25
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 100
print(temp.celsius) # 37.777...
# 검증 동작
# temp.celsius = -300 # ValueError
5. 다형성 (Polymorphism)¶
5.1 메서드 다형성¶
class Shape:
def area(self):
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# 다형성: 같은 메서드명, 다른 동작
def print_area(shape: Shape):
print(f"Area: {shape.area()}")
shapes = [
Rectangle(4, 5),
Circle(3),
Triangle(6, 8)
]
for shape in shapes:
print_area(shape)
# 출력:
# Area: 20
# Area: 28.27431
# Area: 24.0
5.2 덕 타이핑 (Duck Typing)¶
# "오리처럼 걷고 오리처럼 꽥꽥거리면, 그것은 오리다"
# 파이썬은 객체의 타입보다 객체가 가진 메서드/속성에 집중
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Robot:
def speak(self):
return "Beep boop!"
# 상속 관계 없어도 같은 메서드가 있으면 동일하게 처리
def animal_sound(animal):
print(animal.speak())
# 모두 speak() 메서드가 있으므로 동작
animal_sound(Dog()) # Woof!
animal_sound(Cat()) # Meow!
animal_sound(Robot()) # Beep boop!
6. 특수 메서드 (매직 메서드)¶
6.1 기본 특수 메서드¶
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# 문자열 표현 (사용자용)
def __str__(self):
return f"Point({self.x}, {self.y})"
# 문자열 표현 (개발자용, 디버깅)
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
# 동등성 비교
def __eq__(self, other):
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False
# 해시 (dict 키, set 요소로 사용 가능)
def __hash__(self):
return hash((self.x, self.y))
# 길이
def __len__(self):
return 2 # x, y 두 좌표
# 불리언 변환
def __bool__(self):
return self.x != 0 or self.y != 0
p1 = Point(3, 4)
p2 = Point(3, 4)
p3 = Point(0, 0)
print(str(p1)) # Point(3, 4)
print(repr(p1)) # Point(x=3, y=4)
print(p1 == p2) # True
print(len(p1)) # 2
print(bool(p1)) # True
print(bool(p3)) # False
6.2 연산자 오버로딩¶
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# 덧셈: v1 + v2
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
# 뺄셈: v1 - v2
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
# 곱셈 (스칼라): v * 3
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
# 역방향 곱셈: 3 * v
def __rmul__(self, scalar):
return self.__mul__(scalar)
# 내적: v1 @ v2
def __matmul__(self, other):
return self.x * other.x + self.y * other.y
# 음수: -v
def __neg__(self):
return Vector(-self.x, -self.y)
# 절대값 (크기)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(3 * v1) # Vector(9, 12)
print(v1 @ v2) # 11 (내적)
print(-v1) # Vector(-3, -4)
print(abs(v1)) # 5.0
6.3 컨테이너 프로토콜¶
class Deck:
"""카드 덱 클래스"""
def __init__(self):
suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
self.cards = [f"{rank} of {suit}" for suit in suits for rank in ranks]
# 길이
def __len__(self):
return len(self.cards)
# 인덱싱: deck[0]
def __getitem__(self, index):
return self.cards[index]
# 할당: deck[0] = "Joker"
def __setitem__(self, index, value):
self.cards[index] = value
# 삭제: del deck[0]
def __delitem__(self, index):
del self.cards[index]
# 포함 여부: "Ace of Spades" in deck
def __contains__(self, item):
return item in self.cards
# 이터레이션
def __iter__(self):
return iter(self.cards)
deck = Deck()
print(len(deck)) # 52
print(deck[0]) # 2 of Hearts
print(deck[-1]) # A of Spades
print("A of Spades" in deck) # True
# 슬라이싱도 자동 지원
print(deck[0:3]) # ['2 of Hearts', '3 of Hearts', '4 of Hearts']
# 반복
for card in deck[:5]:
print(card)
7. 추상 클래스¶
from abc import ABC, abstractmethod
class Shape(ABC):
"""추상 기반 클래스"""
@abstractmethod
def area(self):
"""면적 계산 (반드시 구현 필요)"""
pass
@abstractmethod
def perimeter(self):
"""둘레 계산 (반드시 구현 필요)"""
pass
def describe(self):
"""일반 메서드 (상속됨)"""
return f"Area: {self.area()}, Perimeter: {self.perimeter()}"
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# 추상 클래스는 인스턴스화 불가
# shape = Shape() # TypeError
rect = Rectangle(4, 5)
print(rect.describe()) # Area: 20, Perimeter: 18
circle = Circle(3)
print(circle.describe()) # Area: 28.27..., Perimeter: 18.84...
정리¶
OOP 핵심 개념¶
| 개념 | 설명 |
|---|---|
| 클래스 | 객체의 설계도 |
| 객체/인스턴스 | 클래스로 생성된 실체 |
__init__ |
생성자, 초기화 메서드 |
self |
현재 인스턴스 참조 |
| 상속 | 부모 클래스의 속성/메서드 물려받기 |
| 오버라이딩 | 부모 메서드 재정의 |
| 캡슐화 | 데이터 보호, 접근 제어 |
| 다형성 | 같은 인터페이스, 다른 동작 |
| 추상 클래스 | 미완성 메서드를 가진 기반 클래스 |
접근 제어 관례¶
| 형식 | 의미 | 예시 |
|---|---|---|
name |
public | 자유롭게 접근 |
_name |
protected | 내부 사용 권장 |
__name |
private | 네임 맹글링 적용 |
다음 단계¶
OOP 기초를 마쳤다면 다음 레슨으로: - 02_Decorators.md: 데코레이터 - 06_Metaclasses.md: 메타클래스 - 07_Descriptors.md: 디스크립터