클래스 & 객체 & 인스턴스
•
클래스 : 특정 객체를 생성하기 위해 변수와 메소드를 정의하는 일종의 틀
•
객체 : 데이터 또는 식별자에 의해 참조되는 공간
•
인스턴스 : 클래스의 현재 생성된 오브젝트
◦
인스턴스 변수 : 인스턴스내 생성된 변수 = 클래스에서 상속받아 적을 수도 있고, 인스턴스 내에서 새로 생성도 가능
◦
인스턴스 메소드 : 인스턴스의 변수를 다루는 메소드, 첫번째 파라미터로 자신이 자동으로 전달(self)
매직메소드
•
클래스에서 특정한 역할로 지정된 메소드 (__init__)
클래스 변수 & 인스턴스 변수
•
클래스 변수 : 클래스 자체 내에서 생성하는 변수
•
클래스 변수와 인스턴스 변수가 같은 이름인 경우 → 인스턴스 변수를 읽어옴.
class User:
user_count = 0 # 클래스 변수
def __init__(self, name, pw):
self.name = name # 인스턴스 변수
self.pw = pw
User.user_count += 1
def check_pw(self, pw): # 인스턴스 메소드
if self.pw == pw:
print("pass")
else:
print("fail")
user1 = User("kim","1234")
user2 = User("lee","1234")
user3 = User("park","1234")
User.user_count = 70
user1.user_count = 77
print(user1.user_count) # 77
print(user2.user_count) # 70
print(user3.user_count) # 70
Python
복사
데코레이터
•
어떤 것을 꾸미는 것
•
함수의 앞 뒤를 꾸며주는 함수
클래스 메소드 & 인스턴스 메소드
•
인스턴스 메소드 : 인스턴스 변수 값을 읽거나 설정하는 메소드
•
클래스 메소드 : 클래스 변수 값을 읽거나 설정하는 메소드
◦
첫번째 파라미터가 클래스 자기 자신을 자동 전달
◦
해당 파라미터를 cls라 판달
◦
@classmethod라는 데코레이터로 꾸며줌
...
@classMethod
def check_user_count(cls):
return cls.user_coun
User.check_user_count() # 4
user1.check_user_count() # 4
Python
복사
정적 메소드(스태틱메소드)
•
자동 전달되는 파라미터 X
•
메서드 실행이 외부 상태에 영향을 끼치지 않는 순수 함수(pure function)를 만들 때 사용
•
부수 효과가 없고 입력 값이 같은면 언제나 같은 출력 값을 반환
•
인스턴스의 상태를 변화시키지 않는 메서드를 만들 때 사용
단순 계산 → 결과값 도출되는 순수함수를 만들 때 좋다.
...
@staticmethod
def add_two_parameter(param1, param2):
return param1 + param2
User.add_two_parameter(3,3)
Python
복사
추상 클래스
•
메서드의 목록만 가진 클래스, 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용
•
상속받은 클래스는 추상메소드를 구현하지 않아도, import할 때까지 에러는 발생하지 않으나 객체를 생성할 시 에러가 발생
•
오로지 상속에만 사용해야함
•
추상클래스를 만들기 위한 형식
◦
반드시 abc 모듈을 import 해야한다.
◦
추상메소드는 생략하면 기본적인 클래스 기능은 동작, 하지만 추상메소드를 추가한 후에는 객체를 생성하면 에러가 발생
from abc import *
class 추상클래스명(ABC): # 추상 클래스 선언
@abstractmethod # 추상 메서드 정의
def 추상메소드1(self):
pass # 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만듦
@abstractmethod
def 추상메소드2(self):
pass
Python
복사
데이터 클래스
•
클래스를 더 간결하고 가독성 있게 정의할 수 있도록 도와주는 기능
•
@dataclass라는 데코레이터로 꾸며줌
•
특징
◦
__init__메소드 불필요
# 일반 클래스
class Book_UserDefined:
def __init__(self, title, author, pages, genre):
self.title = title
self.author = author
self.pages = pages
self.genre = genre
# 데이터 클래스
@dataclass
class Book:
title: str
author: str
pages: int
genre: str
Python
복사
◦
불변성 지정 가능 @dataclass(frozen=True)
# 일반 클래스
class Book_UserDefined:
__slots__ = ('title', 'author', 'pages', 'genre')
# ... (다른 메소드 및 __init__)
def __setattr__(self, name, value):
raise AttributeError("이 클래스는 불변입니다.")
# 데이터 클래스
@dataclass(frozen=True)
class Book:
title: str
author: str
pages: int
genre: str
Python
복사
◦
데이터 클래스는 repr 및 eq 메소드를 자동으로 구현
# 일반 클래스
class Book_UserDefined:
# ... (다른 메소드)
def __eq__(self, other):
return self.title == other.title and self.author == other.author and self.pages == other.pages and self.genre == other.genre
book1_ud = Book_UserDefined("1984", "George Orwell", 328, "Dystopian")
book2_ud = Book_UserDefined("1984", "George Orwell", 328, "Dystopian")
print(book1_ud == book2_ud) # Output: True (직접 __eq__ 구현)
# 데이터 클래스
book1 = Book("1984", "George Orwell", 328, "Dystopian")
book2 = Book("1984", "George Orwell", 328, "Dystopian")
print(book1 == book2) # Output: True (자동으로 __eq__ 구현)
Python
복사
◦
OOP 설계 5 원칙 (SOLID)
SRP(Single Responsibility Principle) : 단일 책임 원칙 = 원포원
•
클래스 단일 책임
•
하나의 클래스 & 메서드는 단 하나의 기능(책임)을 가져야 한다.
•
단 하나의 기능만 유니크하게 가지는 것을 의미
•
해당 기능이 변경될 때만 클래스가 변경될 수 있다.
•
재사용성은 상승하고 결합도는 최소가 된다.
•
예제
class BookService:
def find_book_by_id(query):
# DB에서 찾아오기
# Log 파일에서 찾은 정보 쓰기
# 해당 클래스는 다음과 같이 분리되어야 한다.
class BookService:
def find_book_by_id(query):
# DB에서 찾아오기
def write_log(log):
# Log 파일에 찾은 정보 쓰기.
Python
복사
OCP(Open Closed Principle) : 개방-폐쇄 원칙 = 추상화
•
소프트웨어의 entity(클래스, 모듈, 메서드)들의 확장은 권장하지만 기존 모듈의 수정은 권장하지 않는다는 의미
•
확장에 대해서는 개방적(open), 수정에 대해서는 폐쇄적(closed)
확장 : 새로운 기능의 추가
•
수정이 되지 않을 부분은 상위 클래스나 인터페이스로, 수정이 될 부분은 하위클래스로 관리
•
새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 코드를 확장할 수 있어야 한다.
[ 확장에 열려있다 ]
- 모듈의 확장성을 보장하는 것을 의미한다.
- 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 애플리케이션의 기능을 큰 힘을 들이지 않고 확장할 수 있다.
[ 변경에 닫혀있다 ]
- 객체를 직접적으로 수정하는건 제한해야 한다는 것을 의미한다.
- 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정해야 한다면 새로운 변경사항에 대해 유연하게 대응할 수 없는 애플리케이션이라고 말한다.
- 이는 유지보수의 비용 증가로 이어지는 매우 안좋은 예시이다.
- 따라서 객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야 한다. 그래서 변경에 닫혀있다고 표현한 것이다.
•
추상화 설계
1.
먼저 변경(확장)될 것과 변하지 않을 것을 엄격히 구분
2.
이 두 모듈이 만나는 지점에 추상화(추상클래스 or 인터페이스)를 정의
3.
구현체에 의존하기보다 정의한 추상화에 의존하도록 코드를 작성
•
예제
from abc import ABC, abstractmethod
# 추상화
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Cat(Animal): # 상속
def speak(self):
print("냐옹")
class Dog(Animal): # 상속
def speak(self):
print("멍멍")
# 추상클래스를 상속만 하면 메소드 강제 구현 규칙으로 규격화만 하면 확장에 제한 없다 (opened)
class Sheep(Animal):
def speak(self):
print("매에에");
class HelloAnimal:
def hello(self, animal: Animal):
animal.speak()
def main():
hello = HelloAnimal()
cat = Cat()
dog = Dog()
sheep = Sheep()
hello.hello(cat) # 냐옹
hello.hello(dog) # 멍멍
hello.hello(sheep) # 매에에
if __name__ == "__main__":
main()
Python
복사
LSP(Liskov Substitution Principle) : 리스코프 치환 법칙 = 다형성
•
다형성을 지원하기 위한 원칙
다형성 : 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미
•
협업하는 개발자 사이의 신뢰를 위한 원칙
•
자식 클래스는 언제나 자신의 부모 클래스로 교체될 수 있다는 원칙
•
자식 클래스가 어떻게 구현이 되었든 부모 클래스를 알면 사용이 가능하여야 한다.
⇒ A is B라는 상속 관계가 있을때, B는 자식 클래스 이고 A는 부모클래스
•
이때 B의 행위는 A의 행위의 예상할 수 있는 범위내에서 이루어져야 한다.
•
자식 클래스는 부모 클래스와 호환되어야 하며, 부모 클래스로 교체해도 프로그램의 동작이 일관되게 유지되어야 한다.
•
다형성 설계
1.
매개변수의 타입이 부모/자식 클래스가 같아야 한다.
overriding을 한다고 해서 매개변수 타입이 달라지면 일관성이 떨어짐
2.
반환되는 타입이 같아야 한다.
◦
잘못된 상속 관계 구성으로 인한 메서드 정의
◦
올바르게 별도 인터페이스로 지정 설계한 메서드 정의
1. 매개변수의 타입이 부모/자식 클래스가 같아야 한다.
overriding을 한다고 해서 매개변수 타입이 달라지면 일관성이 떨어진다.
2. 반환되는 타입이 같아야 한다.
1. 매개변수의 타입이 부모/자식 클래스가 같아야 한다.
overriding을 한다고 해서 매개변수 타입이 달라지면 일관성이 떨어진다.
2. 반환되는 타입이 같아야 한다.
1. 매개변수의 타입이 부모/자식 클래스가 같아야 한다.
overriding을 한다고 해서 매개변수 타입이 달라지면 일관성이 떨어진다.
2. 반환되는 타입이 같아야 한다.
•
예제
from abc import ABC, abstractmethod
# 추상 클래스
class Animal(ABC):
pass
# 인터페이스 역할을 하는 추상 클래스
class Speakable(ABC):
@abstractmethod
def speak(self):
pass
# Cat 클래스는 Animal을 상속받고 Speakable을 구현
class Cat(Animal, Speakable):
def speak(self):
print("냐옹")
# Dog 클래스는 Animal을 상속받고 Speakable을 구현
class Dog(Animal, Speakable):
def speak(self):
print("멍멍")
# Fish 클래스는 Animal을 상속받지만 Speakable을 구현하지 않음
class Fish(Animal):
pass
# 테스트 코드
def main():
cat = Cat()
dog = Dog()
fish = Fish()
cat.speak() # 냐옹
dog.speak() # 멍멍
# fish.speak() # AttributeError: 'Fish' object has no attribute 'speak'
if __name__ == "__main__":
main()
Python
복사
ISP(Interface Segregation Principle) : 인터페이스 분리 원칙 = 원포원
•
인터페이스 단일 책임
•
하나의 일반적인 인터페이스보다는, 여러개의 구체적인 인터페이스를 구현해서 사용해야 한다는 원칙
◦
인터페이스는 python에서는 추상클래스라고 이해
◦
General한 추상클래스 하나를 만들지 말고 각 특성별로 추상클래스
•
한 번 인터페이스를 분리하여 구성해놓고 이후 수정사항이 생겨서 또 인터페이스들을 분리하는 행위는 금지
•
인터페이스를 클라이언트에 맞게 분리하여, 클라이언트가 불필요한 메서드에 의존하지 않도록 해야 한다.
•
설계
◦
각각의 기능에 맞게 인터페이스(추상 클래스)를 잘게 분리하도록 구성
•
예제
# 잘못된 예시: 모든 클라이언트가 모든 메서드를 구현해야 함
class Worker:
def work(self):
pass
def eat(self):
pass
class Robot(Worker):
def work(self):
# 작업 수행
pass
def eat(self):
# 로봇은 먹지 않음
pass
# 올바른 예시: 인터페이스를 분리하여 필요한 메서드만 구현
class Workable:
def work(self):
pass
class Eatable:
def eat(self):
pass
class Human(Workable, Eatable):
def work(self):
# 작업 수행
pass
def eat(self):
# 식사
pass
class Robot(Workable):
def work(self):
# 작업 수행
pass
Python
복사
•
주의사항
◦
책임을 준수하더라도 실무에서는 ISP가 만족되지 않을 수 있는 케이스가 존재
DIP(Dependency Inversion Principle) : 의존 역전 법칙 = 결합도 ↓
•
어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그대상의 상위 요소(추상 클래스 or 인터페이스)로 참조
•
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
•
구체적인 클래스보다 인터페이스나 추상 클래스에 의존해야 한다.
•
설계
◦
추상클래스가 가장 메인이 되어야 한다. 나머지 클래스들은 추상클래스에 의존해야 한다.
# 잘못된 예시: 고수준 모듈이 저수준 모듈에 의존
class LightBulb:
def turn_on(self):
pass
def turn_off(self):
pass
class Switch:
def __init__(self, light_bulb):
self.light_bulb = light_bulb
def operate(self, on):
if on:
self.light_bulb.turn_on()
else:
self.light_bulb.turn_off()
# 올바른 예시: 추상화에 의존
class Switchable:
def turn_on(self):
pass
def turn_off(self):
pass
class LightBulb(Switchable):
def turn_on(self):
pass
def turn_off(self):
pass
class Switch(Switchable):
def __init__(self, device: Switchable):
self.device = device
def operate(self, on):
if on:
self.device.turn_on()
else:
self.device.turn_off()
Python
복사