OOP
1. 프로그래밍 패러다임: 절차 지향 vs 객체 지향
1-1. 절차 지향(Procedural Programming)
생각 방식
- “무엇을 먼저 하고, 그 다음에 무엇을 할까?”
- 즉, 데이터는 따로, 그 데이터를 처리하는 함수(절차)를 따로 두고 순서대로 실행한다.
name = "Alice"
age = 25
def introduce(name, age):
print(f"이름은 {name}이고, 나이는 {age}살입니다.")
introduce(name, age)특징
- 함수 중심, “위에서부터 아래로” 흐르는 순차적인 코드
- 데이터가 여러 함수 사이를 왔다 갔다 하면서 사용된다.
- 프로그램이 커지면
- “이 데이터가 지금 어디에서 어떻게 바뀌고 있지?” 추적하기 힘들어짐
- 관련된 데이터/기능이 여기저기 흩어져 있기 쉽다.
1-2. 객체 지향(Object Oriented Programming)
생각 방식
- “이 문제를 해결하기 위해 필요한 ’대상(객체)’가 뭐지?”
- 데이터와 그 데이터를 다루는 함수를 하나의 묶음(객체) 으로 만든다.
class Person:
def __init__(self, name, age): # 생성자
self.name = name
self.age = age
def introduce(self): # 메서드(행동)
print(f"이름은 {self.name}이고, 나이는 {self.age}살입니다.")
alice = Person("Alice", 25) # 객체(인스턴스) 생성
alice.introduce() # 객체가 스스로 소개한다특징
- 클래스: 설계도
- 인스턴스(객체): 설계도로부터 실제로 찍어낸 개체
- 데이터와 기능이 한 덩어리로 묶여 있어서
- 관련 코드가 한 곳에 모여 있다.
- 객체끼리 메시지를 주고받으며 동작하는 구조로 생각하기 쉽다.
1-3. 둘의 차이를 한 줄로 요약하면
- 절차 지향:
- “외부에서 데이터를 들고 다니면서 함수에 계속 넘겨준다.”
- 객체 지향:
- “데이터와 함수를 객체 안에 묶어 넣고, 객체에게 시킨다.”
둘은 상반되는 개념이라기보다 관점과 설계 방법이 다른 것이라고 이해하면 편하다.
2. 객체(Object)와 클래스(Class)
2-1. 객체란?
- 프로그램 안에서 실제로 사용되는 모든 것
- 숫자, 문자열, 리스트, 사용자 정의 클래스의 인스턴스까지 전부 “객체”
- 각각은 상태(데이터) 와 행동(메서드) 를 가진다.
예시: “가수” 객체를 상상해보기
- 상태(속성, attribute)
- 이름, 나이, 데뷔연도, 소속사 …
- 행동(메서드, method)
- 노래하기, 춤추기, 인사하기 …
2-2. 클래스란?
- 객체를 만들기 위한 설계도, 틀
- “가수”라는 클래스를 만들면, 아이유, BTS, 뉴진스 멤버 등은
그 설계도로부터 만들어진 인스턴스가 된다.
class Singer: # 가수 설계도
def __init__(self, name, debut_year):
self.name = name
self.debut_year = debut_year
def sing(self):
print(f"{self.name}이(가) 노래를 부릅니다!")3. 클래스와 인스턴스
3-1. 클래스 정의 기본 형식
class MyClass: # 클래스 이름은 보통 PascalCase
pass # 일단 비워둘 때 사용조금 더 실제 예시:
class Person:
def __init__(self, name, age): # 생성자 메서드
self.name = name
self.age = age
def introduce(self): # 인스턴스 메서드
print(f"안녕하세요, 저는 {self.name}, {self.age}살입니다.")- __init__ : 인스턴스가 만들어질 때 자동으로 호출되는 생성자
- self : “지금 이 인스턴스 자신”을 가리키는 이름 (관례적으로 self 사용)
3-2. 인스턴스 만들기 & 사용
alice = Person("Alice", 25) # 인스턴스 생성
bob = Person("Bob", 30)
alice.introduce() # 안녕하세요, 저는 Alice, 25살입니다.
bob.introduce() # 안녕하세요, 저는 Bob, 30살입니다.여기서 기억할 것:
- 클래스: Person
- 인스턴스(객체): alice, bob
- 인스턴스 메서드를 쓸 때는 alice.introduce() 처럼 점(.)으로 호출한다.
4. 클래스 변수 vs 인스턴스 변수
4-1. 인스턴스 변수
- 인스턴스마다 각자 다른 값을 가지는 변수
- 보통 __init__ 안에서 self.변수명 형태로 정의한다.
class Circle:
def __init__(self, radius):
self.radius = radius # 인스턴스 변수
c1 = Circle(1)
c2 = Circle(2)
print(c1.radius) # 1
print(c2.radius) # 24-2. 클래스 변수
- 클래스 자체에 붙어 있는 변수
- 모든 인스턴스가 공유하는 값
class Circle:
pi = 3.14 # 클래스 변수
def __init__(self, radius):
self.radius = radius
c1 = Circle(1)
c2 = Circle(2)
print(c1.pi, c2.pi) # 3.14 3.14
Circle.pi = 3.14159
print(c1.pi, c2.pi) # 3.14159 3.14159 (둘 다 같이 바뀐다)- Circle.pi 처럼 클래스 이름으로 접근하는 게 기본
- 인스턴스에서도 c1.pi처럼 접근은 가능하지만,
“어? 이게 공용 값이구나”를 알아보기 위해서 가급적 클래스이름.변수 형태로 쓰는 습관이 좋다.
5. 메서드의 세 종류
5-1. 인스턴스 메서드(instance method)
- 가장 기본적인 메서드
- 첫 번째 인자로 항상 self 를 받는다.
- 인스턴스의 상태(속성)를 읽거나 바꾸는 역할.
class Counter:
def __init__(self):
self.count = 0
def increment(self): # 인스턴스 메서드
self.count += 1
c = Counter()
c.increment()
print(c.count) # 15-2. 클래스 메서드(class method)
- 클래스 전체와 관련된 동작을 만들 때 사용
- 첫 번째 인자로 self 대신 cls 를 받는다.
- 데코레이터 @classmethod 를 붙여서 만든다.
class Person:
species = "Human"
def __init__(self, name):
self.name = name
@classmethod
def change_species(cls, new_species):
cls.species = new_species
p1 = Person("Alice")
p2 = Person("Bob")
Person.change_species("Android")
print(p1.species, p2.species) # Android Android→ 인스턴스 개별 상태가 아니라, 클래스 수준의 값을 다룰 때 사용.
5-3. 정적 메서드(static method)
- 클래스, 인스턴스와 상태를 공유하지 않는 독립적인 함수인데,
“의미상 이 클래스 안에 두면 좋겠다” 싶을 때 넣는 메서드.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
print(MathUtils.add(3, 5)) # 8- self, cls 아무것도 받지 않는다는 게 특징.
- “이 기능은 이 클래스와 연관은 있는데, 상태를 건드리지는 않는다” → @staticmethod.
5-4. 생성자 메서드(특별한 인스턴스 메서드)
- __init__
- 인스턴스가 만들어질 때 자동 호출
- 보통 인스턴스 변수의 초기값을 설정하는 역할.
class BankAccount:
interest_rate = 0.02 # 클래스 변수: 이자율
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance6. 메서드 활용 예시 – 은행 계좌 클래스
하나의 예시를 통해 클래스/인스턴스 변수와 메서드가 어떻게 섞여 쓰이는지 보기.
class BankAccount:
interest_rate = 0.02 # 공통 이자율 (클래스 변수)
def __init__(self, owner, balance=0):
self.owner = owner # 계좌 주인 (인스턴스 변수)
self.balance = balance # 잔액
def deposit(self, amount): # 입금
self.balance += amount
def withdraw(self, amount): # 출금
if amount <= self.balance:
self.balance -= amount
else:
print("잔액 부족!")
@classmethod
def set_interest_rate(cls, rate): # 이자율 변경(클래스 메서드)
cls.interest_rate = rate
def add_interest(self): # 이자 적용(인스턴스 메서드)
self.balance += self.balance * self.interest_rate
@staticmethod
def is_positive(amount): # 금액이 양수인지 검사(정적 메서드)
return amount > 0사용 예:
alice = BankAccount("Alice", 1000)
alice.deposit(500) # 잔액 1500
alice.withdraw(200) # 잔액 1300
BankAccount.set_interest_rate(0.05)
alice.add_interest() # 5% 이자 적용
print(alice.balance)
print(BankAccount.is_positive(-10)) # False7. 상속(Inheritance)
7-1. 상속의 기본 개념
- 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 것.
- 같은 종류의 객체들이 공통으로 가지는 부분은 부모에 두고,
각자의 차이점은 자식 클래스에만 추가하는 방식.
class Animal:
def eat(self):
print("냠냠 먹습니다.")
class Dog(Animal): # Animal을 상속받는 Dog
def bark(self):
print("멍멍!")
my_dog = Dog()
my_dog.eat() # 부모에게서 물려받은 메서드
my_dog.bark() # 자식이 가진 메서드7-2. 상속이 필요한 이유
- 코드 재사용
- 공통 기능은 부모 클래스에 한 번만 써두고, 재사용 가능.
- 유지보수 용이
- 공통 로직 수정 → 부모만 고치면 자식 전부에 반영.
- 구조적 설계
- “사람 → 학생/교수”, “동물 → 포유류/조류” 이런 관계를 코드에 그대로 표현.
7-3. 사람 → 학생/교수 예시
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print(f"{self.name}, {self.age}살입니다.")
class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age) # 부모 초기화
self.student_id = student_id
def talk(self):
print(f"저는 학생이고 학번은 {self.student_id}입니다.")
class Professor(Person):
def __init__(self, name, age, department):
super().__init__(name, age)
self.department = department
def talk(self):
print(f"저는 교수이고 소속 학과는 {self.department}입니다.")- Student, Professor는 Person의 자식 클래스
- introduce()는 공통 기능 → 부모(Person)에 한 번 정의
- 각자의 talk() 메서드는 다르게 동작 (오버라이딩, 아래에서 설명).
8. 메서드 오버라이딩(Overriding)
8-1. 개념
- 부모 클래스에 있던 메서드를 자식 클래스에서 다시 정의해서 덮어쓰기
- 이름은 같지만, 자식에서 원하는 방식으로 동작을 바꿀 수 있다.
위 Student/Professor 예시에서 talk() 가 바로 오버라이딩 예이다.
s = Student("Alice", 20, "2024001")
p = Professor("Bob", 45, "Computer Science")
s.introduce() # Person의 introduce 사용
s.talk() # Student에서 재정의한 talk
p.introduce() # Person의 introduce 사용
p.talk() # Professor에서 재정의한 talk- 같은 이름의 메서드를 호출해도, 인스턴스의 실제 타입(Student/Professor)에 따라 다른 코드가 실행된다 → 다형성(polymorphism).
9. 다중 상속(Multiple Inheritance)과 MRO
9-1. 다중 상속
- 한 클래스가 여러 부모 클래스로부터 동시에 상속받는 것.
간단 예시:
class ParentA:
def show(self):
print("A")
class ParentB:
def show(self):
print("B")
class Child(ParentA, ParentB):
pass
c = Child()
c.show() # 어떤 show가 불릴까?- 파이썬은 MRO(Method Resolution Order) 라는 규칙에 따라
어떤 부모의 메서드를 먼저 찾을지 순서를 정한다. (일반적으로 왼쪽 → 오른쪽)
다이아몬드 상속(여러 경로로 같은 조상이 중복되는 경우)에서도 MRO가
“한 번만 호출되게” 순서를 잘 계산해 준다.
9-2. super()의 역할
- 부모 클래스의 메서드를 호출할 때 쓰는 도구.
- 단일 상속에서는 super().메서드() 가 직접 부모를 부르지만,
- 다중 상속에서는 MRO에 따른 다음 클래스의 메서드를 호출한다.
단일 상속 예:
class Person:
def __init__(self, name):
self.name = name
class Student(Person):
def __init__(self, name, student_id):
super().__init__(name) # Person.__init__ 호출
self.student_id = student_id다중 상속 예(개략):
class A:
def __init__(self):
print("A")
super().__init__()
class B:
def __init__(self):
print("B")
super().__init__()
class C(A, B):
def __init__(self):
print("C")
super().__init__()
c = C()
# C -> A -> B 순서로 super가 이어서 호출됨 (MRO에 따라)10. 에러와 예외, 디버깅
10-1. 버그와 디버깅
- 버그: 프로그램이 원래 의도와 다르게 동작하게 만드는 오류
- 디버깅: 그 오류를 찾고 고치는 과정
- print 찍어보기
- IDE의 디버거 사용
- 에러 메세지 꼼꼼히 읽기 등등
10-2. 문법 오류(Syntax Error) vs 예외(Exception)
- 문법 오류
- 파이썬이 코드를 읽는 단계에서
“이건 문법에 맞지 않는다” 하고 바로 중단하는 오류. - 예: 괄호 안 닫힘, if 뒤에 : 빠짐 등.
- 파이썬이 코드를 읽는 단계에서
- 예외(Exception)
- 문법은 맞지만, 실행 중에 문제가 생겼을 때 발생.
- 예: 0으로 나누기, 리스트 인덱스 범위 초과, 존재하지 않는 키 접근 등.
10-3. 자주 보는 내장 예외들
- ZeroDivisionError : 0으로 나눴을 때
- NameError : 정의되지 않은 이름 사용
- TypeError : 타입이 맞지 않을 때 (예: 숫자 + 문자열)
- ValueError : 타입은 맞는데 값이 이상할 때 (예: int("abc"))
- IndexError : 리스트/튜플 인덱스 범위 벗어남
- KeyError : 딕셔너리에 없는 키를 조회할 때
- ImportError, ModuleNotFoundError : 잘못된 모듈 import 시도
- IndentationError : 들여쓰기 잘못했을 때 등.
11. 예외 처리 – try / except / else / finally
11-1. 기본 구조
try:
# 에러(예외)가 발생할 수 있는 코드
result = 10 / x
except ZeroDivisionError:
# 위에서 지정한 예외가 발생했을 때 실행할 코드
print("0으로 나눌 수 없습니다.")- try 블록 안에서 ZeroDivisionError가 일어나면
프로그램이 바로 죽지 않고, except로 흐름이 넘어간다.
11-2. 여러 예외 처리
try:
num = int(input("100을 나눌 숫자를 입력하세요: "))
print(100 / num)
except ZeroDivisionError:
print("0으로는 나눌 수 없습니다.")
except ValueError:
print("정수를 입력해야 합니다.")- 서로 다른 예외를 각각 다른 방식으로 처리하고 싶으면 except를 여러 개 둔다.
11-3. else와 finally
try:
num = int(input("100을 나눌 숫자를 입력하세요: "))
result = 100 / num
except (ZeroDivisionError, ValueError):
print("입력이 잘못되었습니다.")
else:
print("결과:", result) # 예외가 하나도 없었을 때만 실행
finally:
print("프로그램을 종료합니다.") # 예외와 상관 없이 항상 실행- else : 예외가 발생하지 않았을 때만 실행
- finally : 예외 여부와 상관 없이 항상 실행 (파일 닫기, DB 연결 종료 등에 많이 사용)
11-4. 예외 객체 다루기
my_list = [1, 2, 3]
try:
print(my_list[5])
except IndexError as error:
print("에러 타입:", type(error))
print("에러 메시지:", error)- as error 로 예외 객체를 받아서,
에러 메시지나 타입을 확인할 수 있다.
12. EAFP vs LBYL
12-1. 두 가지 스타일
- EAFP (Easier to Ask Forgiveness than Permission)
- “먼저 시도해 보고, 안 되면 사과(예외 처리)하자”
- 파이썬스러운 스타일 (try / except 중심)
# EAFP 스타일
try:
value = my_dict['key']
except KeyError:
print("키가 없습니다.")- LBYL (Look Before You Leap)
- “하기 전에 먼저 조건을 꼼꼼히 검사하자”
- if로 다 체크한 후 실행하는 스타일
# LBYL 스타일
if 'key' in my_dict:
value = my_dict['key']
else:
print("키가 없습니다.")- 파이썬에서는 보통 EAFP 스타일이 더 자연스럽다고 많이들 말한다.
- 하지만 상황에 따라 LBYL이 더 읽기 좋을 때도 있으니 둘 다 알고 쓰면 된다.
오늘 복습
- 객체, 클래스, 인스턴스, 속성, 메서드
- 인스턴스 변수 vs 클래스 변수
- 인스턴스 / 클래스 / 정적 메서드, 생성자 __init__
- 상속, 메서드 오버라이딩, 다중 상속, MRO, super()
- 버그, 디버깅, 문법 오류 vs 예외
- 내장 예외 종류들
- try / except / else / finally 구조
- EAFP vs LBYL
\
'Language& Framework > Python' 카테고리의 다른 글
| Data Structure (0) | 2025.12.12 |
|---|---|
| Functions. & Module Control of Flow (0) | 2025.12.12 |
| Python Basic Syntax (0) | 2025.12.11 |