- 객체 지향 프로그래밍 (OOP) 개요
- 클래스 정의
- 생성자와 소멸자
- 속성과 메소드
- is 연산자와 id() 함수
- dict 속성
- 클래스 변수 vs 인스턴스 변수
- 클래스 메소드 vs 인스턴스 메소드
- 정적 메소드
- 프로퍼티 (Property)
- 실전 예제
# 데이터와 동작이 분리됨
def goto(name, address):
print(f"{name}이(가) {address}으로 갑니다.")
# 함수에 데이터를 인자로 전달
goto("홍길동", "서울시 강남구")
goto("김철수", "부산시 해운대구")# 데이터(속성)와 동작(메소드)이 객체로 결합
class Person:
def __init__(self, name, address):
self.name = name # 속성
self.address = address # 속성
def go_home(self): # 메소드
print(f"{self.name}이(가) {self.address}으로 갑니다.")
# 객체 생성 및 사용
hong = Person("홍길동", "서울시 강남구")
kim = Person("김철수", "부산시 해운대구")
hong.go_home() # 출력: 홍길동이(가) 서울시 강남구으로 갑니다.
kim.go_home() # 출력: 김철수이(가) 부산시 해운대구으로 갑니다.| 개념 | 영문 | 설명 |
|---|---|---|
| 클래스 | Class | 객체를 정의하는 설계도(틀) |
| 객체 | Object | 클래스로부터 생성된 실체(인스턴스) |
| 속성 | Attribute | 객체가 가진 데이터(변수) |
| 메소드 | Method | 객체가 수행하는 동작(함수) |
| 인스턴스 | Instance | 클래스로부터 만들어진 개별 객체 |
| 장점 | 설명 |
|---|---|
| 코드 재사용성 | 클래스 정의 후 여러 객체 생성 가능 |
| 유지보수 용이 | 관련 데이터와 기능이 한 곳에 모임 |
| 확장성 | 상속을 통한 기능 확장 |
| 캡슐화 | 내부 구현을 숨기고 인터페이스만 노출 |
| 다형성 | 동일한 인터페이스로 다양한 동작 수행 |
class ClassName:
"""클래스 설명 (docstring)"""
pass
# 예시
class Cookie:
"""쿠키를 표현하는 클래스"""
pass
# 객체 생성
cookie1 = Cookie()
cookie2 = Cookie()
cookie3 = Cookie()
print(type(cookie1)) # <class '__main__.Cookie'>| 규칙 | 설명 | 예시 |
|---|---|---|
| PascalCase | 각 단어 첫 글자 대문자 | MyClass, UserAccount |
| 명사 사용 | 클래스는 사물/개념 표현 | Person, Car, Database |
| 단수형 | 하나의 객체를 표현 | Student (not Students) |
self는 인스턴스 자신을 참조하는 매개변수입니다.
class Rectangle:
def __init__(self, width, height):
# self를 통해 인스턴스 속성에 접근
self.width = width
self.height = height
def get_area(self):
# self를 통해 다른 속성에 접근
return self.width * self.height
def get_info(self):
# self를 통해 다른 메소드 호출
area = self.get_area()
return f"가로: {self.width}, 세로: {self.height}, 넓이: {area}"
rect = Rectangle(10, 20)
print(rect.get_info())
# 출력: 가로: 10, 세로: 20, 넓이: 200객체가 생성될 때 자동으로 호출되며, 초기화 작업을 수행합니다.
class Account:
def __init__(self, owner, balance=0):
"""생성자: 계좌 초기화"""
print(f"계좌 생성: {owner}")
self.owner = owner
self.balance = balance
self.transaction_count = 0
# 객체 생성 시 __init__ 자동 호출
acc1 = Account("홍길동", 10000)
# 출력: 계좌 생성: 홍길동
acc2 = Account("김철수") # 기본값 사용
# 출력: 계좌 생성: 김철수
print(acc1.balance) # 10000
print(acc2.balance) # 0객체가 소멸될 때 자동으로 호출되며, 정리 작업을 수행합니다.
class FileHandler:
def __init__(self, filename):
print(f"파일 열기: {filename}")
self.filename = filename
# 실제로는 파일을 열 것임
def __del__(self):
print(f"파일 닫기: {self.filename}")
# 실제로는 파일을 닫을 것임
# 객체 생성
handler = FileHandler("data.txt")
# 출력: 파일 열기: data.txt
# 명시적 삭제
del handler
# 출력: 파일 닫기: data.txtPython은 메소드 오버로딩을 지원하지 않지만, 다양한 방법으로 유사한 기능을 구현할 수 있습니다.
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
@classmethod
def from_tuple(cls, coords):
"""튜플로부터 Point 생성"""
return cls(coords[0], coords[1])
@classmethod
def from_string(cls, coord_str):
"""문자열로부터 Point 생성 (예: '3,4')"""
x, y = map(int, coord_str.split(','))
return cls(x, y)
# 다양한 방법으로 객체 생성
p1 = Point(3, 4)
p2 = Point.from_tuple((5, 6))
p3 = Point.from_string("7,8")
print(f"p1: ({p1.x}, {p1.y})") # p1: (3, 4)
print(f"p2: ({p2.x}, {p2.y})") # p2: (5, 6)
print(f"p3: ({p3.x}, {p3.y})") # p3: (7, 8)class Car:
def __init__(self, brand, model, year):
# 인스턴스 속성 정의
self.brand = brand
self.model = model
self.year = year
self.mileage = 0
self.is_running = False
# 속성 접근
car = Car("현대", "소나타", 2023)
print(car.brand) # 현대
print(car.year) # 2023
# 속성 수정
car.mileage = 1000
print(car.mileage) # 1000Python은 런타임에 속성을 동적으로 추가할 수 있습니다.
class Person:
def __init__(self, name):
self.name = name
person = Person("홍길동")
# 동적으로 속성 추가
person.age = 25
person.city = "서울"
print(person.age) # 25
print(person.city) # 서울
# 다른 인스턴스에는 해당 속성이 없음
person2 = Person("김철수")
# print(person2.age) # AttributeError!class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
"""입금 메소드"""
if amount > 0:
self.balance += amount
return f"{amount}원 입금. 잔액: {self.balance}원"
return "입금액은 0보다 커야 합니다."
def withdraw(self, amount):
"""출금 메소드"""
if amount > self.balance:
return "잔액이 부족합니다."
if amount <= 0:
return "출금액은 0보다 커야 합니다."
self.balance -= amount
return f"{amount}원 출금. 잔액: {self.balance}원"
def get_balance(self):
"""잔액 조회 메소드"""
return self.balance
# 메소드 사용
account = BankAccount("홍길동", 10000)
print(account.deposit(5000)) # 5000원 입금. 잔액: 15000원
print(account.withdraw(3000)) # 3000원 출금. 잔액: 12000원
print(account.get_balance()) # 12000class Calculator:
def add(self, a, b):
return a + b
calc = Calculator()
# 인스턴스를 통한 호출 (일반적)
result1 = calc.add(3, 5)
print(result1) # 8
# 클래스를 통한 호출 (self를 명시적으로 전달)
result2 = Calculator.add(calc, 3, 5)
print(result2) # 8
# 내장 타입도 동일한 방식
print("hello".upper()) # HELLO
print(str.upper("hello")) # HELLO두 객체가 **동일한 객체(메모리 주소)**인지 확인합니다.
class Box:
def __init__(self, value):
self.value = value
box1 = Box(10)
box2 = Box(10)
box3 = box1 # 같은 객체 참조
# is 연산자: 동일 객체 여부
print(box1 is box2) # False (값은 같지만 다른 객체)
print(box1 is box3) # True (같은 객체)
# == 연산자: 값 비교 (기본적으로 is와 동일, 재정의 가능)
print(box1 == box2) # False (기본 동작)
print(box1 == box3) # True| 연산자 | 비교 대상 | 설명 |
|---|---|---|
is |
정체성 (Identity) | 메모리 주소가 같은지 |
== |
동등성 (Equality) | 값이 같은지 (재정의 가능) |
# 리스트 예제
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 == list2) # True (값이 같음)
print(list1 is list2) # False (다른 객체)
print(list1 is list3) # True (같은 객체)
# 작은 정수는 캐싱됨 (Python 최적화)
a = 256
b = 256
print(a is b) # True (캐싱된 객체)
c = 257
d = 257
print(c is d) # False (새로 생성됨) - 실행 환경에 따라 다를 수 있음객체의 **고유 ID(메모리 주소)**를 반환합니다.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = p1
print(f"p1 id: {id(p1)}") # 예: 140234567890
print(f"p2 id: {id(p2)}") # 예: 140234567891 (다름)
print(f"p3 id: {id(p3)}") # 예: 140234567890 (p1과 같음)
# id 비교 = is 연산자와 동일
print(id(p1) == id(p3)) # True
print(p1 is p3) # Trueclass Animal:
pass
class Dog(Animal):
pass
dog = Dog()
# type() 사용 - 정확한 타입만 확인
print(type(dog) is Dog) # True
print(type(dog) is Animal) # False
# isinstance() 사용 - 상속 관계 포함
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True (상속)
print(isinstance(dog, object)) # True (모든 클래스의 최상위)객체가 가진 속성과 값을 딕셔너리 형태로 반환합니다.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
self.scores = []
student = Student("홍길동", 3)
student.scores = [90, 85, 88]
# 인스턴스 속성 확인
print(student.__dict__)
# 출력: {'name': '홍길동', 'grade': 3, 'scores': [90, 85, 88]}
# 동적 속성 추가 후 확인
student.email = "hong@example.com"
print(student.__dict__)
# 출력: {'name': '홍길동', 'grade': 3, 'scores': [90, 85, 88], 'email': 'hong@example.com'}클래스가 가진 메소드, 클래스 변수 등을 확인할 수 있습니다.
class Rectangle:
# 클래스 변수
count = 0
def __init__(self, width, height):
self.width = width
self.height = height
Rectangle.count += 1
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# 클래스 속성 확인
print("클래스 __dict__:")
for key, value in Rectangle.__dict__.items():
if not key.startswith('__'):
print(f" {key}: {value}")
# 출력:
# 클래스 __dict__:
# count: 0
# area: <function Rectangle.area at ...>
# perimeter: <function Rectangle.perimeter at ...>class Config:
def __init__(self):
self.debug = False
self.version = "1.0"
config = Config()
# 딕셔너리처럼 속성 접근
print(config.__dict__['debug']) # False
# 딕셔너리처럼 속성 수정
config.__dict__['debug'] = True
print(config.debug) # True
# 딕셔너리처럼 속성 추가
config.__dict__['new_setting'] = "value"
print(config.new_setting) # value
# vars() 함수도 동일한 기능
print(vars(config)) # __dict__와 동일| 구분 | 클래스 변수 | 인스턴스 변수 |
|---|---|---|
| 선언 위치 | 클래스 내부, 메소드 바깥 | self.변수명 |
| 소속 | 클래스 자체 | 각 인스턴스 |
| 공유 여부 | 모든 인스턴스가 공유 | 인스턴스별 독립 |
| 접근 방법 | 클래스명.변수 (권장) |
self.변수 또는 인스턴스.변수 |
| 네임스페이스 | 클래스 네임스페이스 | 인스턴스 네임스페이스 |
class Account:
# 클래스 변수: 모든 인스턴스가 공유
bank_name = "파이썬 은행"
interest_rate = 0.02
total_accounts = 0
def __init__(self, owner, balance=0):
self.owner = owner # 인스턴스 변수
self.balance = balance # 인스턴스 변수
Account.total_accounts += 1 # 클래스 변수 수정
# 인스턴스 생성
acc1 = Account("홍길동", 10000)
acc2 = Account("김철수", 20000)
# 클래스 변수 접근 (권장: 클래스명으로)
print(Account.bank_name) # 파이썬 은행
print(Account.total_accounts) # 2
# 인스턴스를 통해서도 읽기 가능 (비권장)
print(acc1.bank_name) # 파이썬 은행class Student:
school = "파이썬 고등학교" # 클래스 변수
def __init__(self, name, grade):
self.name = name # 인스턴스 변수
self.grade = grade # 인스턴스 변수
s1 = Student("홍길동", 1)
s2 = Student("김철수", 2)
# 인스턴스 변수는 독립적
print(s1.name, s1.grade) # 홍길동 1
print(s2.name, s2.grade) # 김철수 2
# 한 인스턴스 변경이 다른 인스턴스에 영향 없음
s1.grade = 3
print(s1.grade) # 3
print(s2.grade) # 2 (변경 없음)class Counter:
count = 0 # 클래스 변수
c1 = Counter()
c2 = Counter()
# 클래스명으로 수정 (올바른 방법)
Counter.count = 10
print(c1.count) # 10
print(c2.count) # 10
# 인스턴스로 수정하면 새 인스턴스 변수가 생성됨! (주의)
c1.count = 100 # c1에 인스턴스 변수 count 생성
print(c1.count) # 100 (인스턴스 변수)
print(c2.count) # 10 (클래스 변수)
print(Counter.count) # 10 (클래스 변수)
# 확인
print(c1.__dict__) # {'count': 100} - 인스턴스 변수
print(c2.__dict__) # {} - 인스턴스 변수 없음# 잘못된 예: 가변 객체를 클래스 변수로
class BadExample:
shared_list = [] # 위험! 모든 인스턴스가 공유
def add_item(self, item):
self.shared_list.append(item)
b1 = BadExample()
b2 = BadExample()
b1.add_item("a")
b2.add_item("b")
print(b1.shared_list) # ['a', 'b'] - 예상과 다름!
print(b2.shared_list) # ['a', 'b']
# 올바른 예: 인스턴스 변수로 사용
class GoodExample:
def __init__(self):
self.my_list = [] # 인스턴스별 독립
def add_item(self, item):
self.my_list.append(item)
g1 = GoodExample()
g2 = GoodExample()
g1.add_item("a")
g2.add_item("b")
print(g1.my_list) # ['a']
print(g2.my_list) # ['b']첫 번째 매개변수로 self를 받으며, 인스턴스를 통해 호출합니다.
class Circle:
def __init__(self, radius):
self.radius = radius
def get_area(self):
"""인스턴스 메소드: self를 통해 인스턴스 속성 접근"""
return 3.14159 * self.radius ** 2
def get_circumference(self):
"""인스턴스 메소드"""
return 2 * 3.14159 * self.radius
circle = Circle(5)
print(circle.get_area()) # 78.53975
print(circle.get_circumference()) # 31.4159첫 번째 매개변수로 cls(클래스 자체)를 받습니다.
class Employee:
company = "파이썬 주식회사"
employee_count = 0
def __init__(self, name, department):
self.name = name
self.department = department
Employee.employee_count += 1
@classmethod
def get_company(cls):
"""클래스 메소드: 클래스 변수에 접근"""
return cls.company
@classmethod
def get_employee_count(cls):
"""클래스 메소드: 클래스 변수에 접근"""
return cls.employee_count
@classmethod
def from_string(cls, emp_string):
"""팩토리 메소드: 문자열로부터 객체 생성"""
name, department = emp_string.split('-')
return cls(name, department)
# 클래스 메소드 호출 (인스턴스 없이)
print(Employee.get_company()) # 파이썬 주식회사
# 객체 생성
emp1 = Employee("홍길동", "개발팀")
emp2 = Employee.from_string("김철수-마케팅팀")
print(Employee.get_employee_count()) # 2
print(emp2.name, emp2.department) # 김철수 마케팅팀class Utility:
def regular_method(): # self 없음
"""클래스 이름으로만 호출 가능"""
print("클래스 메소드처럼 동작")
# 클래스 이름으로 호출
Utility.regular_method() # OK
# 인스턴스로 호출 시 에러
util = Utility()
# util.regular_method() # TypeError: takes 0 positional arguments but 1 was given| 구분 | 인스턴스 메소드 | 클래스 메소드 |
|---|---|---|
| 데코레이터 | 없음 | @classmethod |
| 첫 번째 매개변수 | self (인스턴스) |
cls (클래스) |
| 접근 대상 | 인스턴스 속성, 클래스 속성 | 클래스 속성만 |
| 호출 방법 | 인스턴스.메소드() |
클래스.메소드() 또는 인스턴스.메소드() |
| 용도 | 인스턴스별 동작 | 클래스 관련 동작, 팩토리 메소드 |
self도 cls도 받지 않으며, 클래스와 논리적으로 관련된 독립적인 함수입니다.
class MathUtils:
@staticmethod
def add(a, b):
"""정적 메소드: 클래스/인스턴스와 무관한 유틸리티"""
return a + b
@staticmethod
def multiply(a, b):
return a * b
@staticmethod
def is_even(num):
return num % 2 == 0
# 클래스 이름으로 호출
print(MathUtils.add(3, 5)) # 8
print(MathUtils.is_even(4)) # True
# 인스턴스로도 호출 가능
math_util = MathUtils()
print(math_util.multiply(3, 4)) # 12class Demo:
class_var = "클래스 변수"
def __init__(self, value):
self.instance_var = value
def instance_method(self):
"""인스턴스 메소드: self, cls, 둘 다 접근 가능"""
return f"인스턴스: {self.instance_var}, 클래스: {self.class_var}"
@classmethod
def class_method(cls):
"""클래스 메소드: cls만 접근 가능, self 불가"""
return f"클래스: {cls.class_var}"
# self.instance_var 접근 불가
@staticmethod
def static_method(x, y):
"""정적 메소드: self, cls 모두 불가, 독립적 함수"""
return x + y
# self, cls 접근 불가
demo = Demo("인스턴스 값")
print(demo.instance_method()) # 인스턴스: 인스턴스 값, 클래스: 클래스 변수
print(Demo.class_method()) # 클래스: 클래스 변수
print(Demo.static_method(1, 2)) # 3속성처럼 접근하지만 실제로는 메소드가 호출되는 기능입니다.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius # 내부 저장용 (관례: _로 시작)
@property
def celsius(self):
"""Getter: 속성 읽기"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter: 속성 쓰기 (유효성 검사 가능)"""
if value < -273.15:
raise ValueError("절대영도 이하는 불가능합니다.")
self._celsius = value
@property
def fahrenheit(self):
"""계산된 속성 (읽기 전용)"""
return self._celsius * 9/5 + 32
# 사용
temp = Temperature(25)
# 속성처럼 접근
print(temp.celsius) # 25 (getter 호출)
print(temp.fahrenheit) # 77.0 (계산된 값)
# 속성처럼 할당
temp.celsius = 30 # setter 호출
print(temp.celsius) # 30
# 유효성 검사
# temp.celsius = -300 # ValueError!| 장점 | 설명 |
|---|---|
| 캡슐화 | 내부 구현을 숨기고 인터페이스 제공 |
| 유효성 검사 | setter에서 값 검증 가능 |
| 계산된 속성 | 다른 속성 기반으로 값 계산 |
| 하위 호환성 | 기존 코드 변경 없이 동작 추가 |
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("너비는 양수여야 합니다.")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("높이는 양수여야 합니다.")
self._height = value
@property
def area(self):
"""읽기 전용 계산 속성"""
return self._width * self._height
@property
def perimeter(self):
"""읽기 전용 계산 속성"""
return 2 * (self._width + self._height)
rect = Rectangle(10, 20)
print(rect.area) # 200
print(rect.perimeter) # 60
rect.width = 15
print(rect.area) # 300 (자동 재계산)class BankAccount:
"""은행 계좌 클래스"""
bank_name = "Python Bank"
interest_rate = 0.02
_account_counter = 0
def __init__(self, owner, initial_balance=0):
BankAccount._account_counter += 1
self._account_number = f"ACC{BankAccount._account_counter:06d}"
self._owner = owner
self._balance = initial_balance
self._transactions = []
@property
def account_number(self):
return self._account_number
@property
def owner(self):
return self._owner
@property
def balance(self):
return self._balance
def deposit(self, amount):
"""입금"""
if amount <= 0:
raise ValueError("입금액은 양수여야 합니다.")
self._balance += amount
self._transactions.append(('입금', amount))
return self._balance
def withdraw(self, amount):
"""출금"""
if amount <= 0:
raise ValueError("출금액은 양수여야 합니다.")
if amount > self._balance:
raise ValueError("잔액이 부족합니다.")
self._balance -= amount
self._transactions.append(('출금', amount))
return self._balance
def get_transaction_history(self):
"""거래 내역 조회"""
return self._transactions.copy()
def apply_interest(self):
"""이자 적용"""
interest = self._balance * self.interest_rate
self._balance += interest
self._transactions.append(('이자', interest))
return interest
@classmethod
def get_total_accounts(cls):
return cls._account_counter
def __str__(self):
return f"[{self.account_number}] {self.owner}: {self.balance:,}원"
def __repr__(self):
return f"BankAccount('{self.owner}', {self.balance})"
# 사용 예제
acc1 = BankAccount("홍길동", 100000)
acc2 = BankAccount("김철수", 50000)
acc1.deposit(50000)
acc1.withdraw(30000)
acc1.apply_interest()
print(acc1)
# 출력: [ACC000001] 홍길동: 122,400원
print(f"총 계좌 수: {BankAccount.get_total_accounts()}")
# 출력: 총 계좌 수: 2
print(acc1.get_transaction_history())
# [('입금', 50000), ('출금', 30000), ('이자', 2400.0)]class Student:
"""학생 클래스"""
school_name = "Python Academy"
_student_count = 0
def __init__(self, name, grade):
Student._student_count += 1
self._id = f"STU{Student._student_count:04d}"
self._name = name
self._grade = grade
self._scores = {}
@property
def id(self):
return self._id
@property
def name(self):
return self._name
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, value):
if not 1 <= value <= 6:
raise ValueError("학년은 1~6이어야 합니다.")
self._grade = value
def add_score(self, subject, score):
"""과목 점수 추가"""
if not 0 <= score <= 100:
raise ValueError("점수는 0~100이어야 합니다.")
self._scores[subject] = score
def get_average(self):
"""평균 점수 계산"""
if not self._scores:
return 0
return sum(self._scores.values()) / len(self._scores)
def get_grade_letter(self):
"""학점 계산"""
avg = self.get_average()
if avg >= 90: return 'A'
if avg >= 80: return 'B'
if avg >= 70: return 'C'
if avg >= 60: return 'D'
return 'F'
@classmethod
def get_student_count(cls):
return cls._student_count
def __str__(self):
return f"{self.name} ({self.grade}학년) - 평균: {self.get_average():.1f}"
# 사용
student = Student("홍길동", 3)
student.add_score("국어", 90)
student.add_score("영어", 85)
student.add_score("수학", 88)
print(student)
# 출력: 홍길동 (3학년) - 평균: 87.7
print(f"학점: {student.get_grade_letter()}")
# 출력: 학점: B| 구분 | 클래스 변수 | 인스턴스 변수 |
|---|---|---|
| 선언 | 클래스 내, 메소드 바깥 | self.변수 |
| 공유 | 모든 인스턴스 공유 | 인스턴스별 독립 |
| 접근 | 클래스명.변수 |
self.변수 |
| 용도 | 상수, 카운터 | 객체 상태 |
| 구분 | 인스턴스 메소드 | 클래스 메소드 | 정적 메소드 |
|---|---|---|---|
| 데코레이터 | 없음 | @classmethod |
@staticmethod |
| 첫 매개변수 | self |
cls |
없음 |
| 접근 가능 | 인스턴스 + 클래스 | 클래스만 | 없음 |
| 용도 | 객체 동작 | 팩토리, 클래스 조작 | 유틸리티 |