일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- flask
- airflow subdag
- 공분산
- Retry
- requests
- subdag
- top_k
- GCP
- UDF
- login crawling
- gather_nd
- correlation
- Counterfactual Explanations
- 유튜브 API
- GenericGBQException
- chatGPT
- tensorflow text
- API
- integrated gradient
- API Gateway
- spark udf
- hadoop
- youtube data
- Airflow
- grad-cam
- BigQuery
- XAI
- session 유지
- 상관관계
- TensorFlow
- Today
- Total
데이터과학 삼학년
SOLID-python 원칙 : clean code 본문
SOLID Principles : improve Object-Oriented Design in Python
SOLID 원칙
- oop ?! → 간단히 말해 pyhton의 class 기능 → 붕어빵 틀을 만들어 여러 붕어빵을 찍어내는 것과 같은 것
- 코드를 보다 효율적이고 유지보수하기 쉬우며, 효과적으로 작성하고자 만든 어떤 원칙이 있는데…대표적인 것이 SOLID 원칙
- SOLID 원칙별로 한글자씩 따서 만든 것으로 생각한 것 처럼 총 5가지 원칙이 있음
S**ingle-responsibility principle (SRP)
L**iskov substitution principle (LSP)
I**nterface segregation principle (ISP)
D**ependency inversion principle (DIP)
Single-responsibility principe (단일 책임 원칙)
- 한 클래스는 하나의 책임만 가져야 한다. → 목적 기능단위로 class를 만드는 것!
- Filemanger라는 class가 있다면 내부 method에 read, write, compress, decompress 가 있다면 단일 책임 원칙을 위배한 거로 볼 수 있음
- 다음과 같이 두개의 class로 나누어 만드는 것이 보다 나음
- Filemanager : write, read
- ZipFilemanager : compress, decompress
# file_manager_srp.py
from pathlib import Path
from zipfile import ZipFile
class FileManager:
def __init__(self, filename):
self.path = Path(filename)
def read(self, encoding="utf-8"):
return self.path.read_text(encoding)
def write(self, data, encoding="utf-8"):
self.path.write_text(data, encoding)
class ZipFileManager:
def __init__(self, filename):
self.path = Path(filename)
def compress(self):
with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:
archive.write(self.path)
def decompress(self):
with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:
archive.extractall()
Open-closed principle (개방 폐쇄 원칙)
- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 메서드를 추가할 수는 있으나, 수정하는 일은 없어야 함
- 객체를 상속받아 여러 class를 만드는 것은 가능하지만, class자체의 변경이 일어나는 것은 지양
- 아래와 같이 도형 모양을 만드는 class가 있을때, 사각형이던 원이던 만들수가 있긴 함
- 그러나 직사각형, 사다리꼴을 만들고 싶다면?! → 내부 method를 모두 수정해야함…elif 같은 조건을 써서…
# shapes_ocp.py
from math import pi
class Shape:
def __init__(self, shape_type, **kwargs):
self.shape_type = shape_type
if self.shape_type == "rectangle":
self.width = kwargs["width"]
self.height = kwargs["height"]
elif self.shape_type == "circle":
self.radius = kwargs["radius"]
def calculate_area(self):
if self.shape_type == "rectangle":
return self.width * self.height
elif self.shape_type == "circle":
return pi * self.radius**2
- 그런식으로 class내 불필요한 기능을 확장할 필요는 없어
- 차라리 Shape라는 기본 껍데기 class를 상속받아, 원, 사각형, 사다리꼴 등 class를 확장하는 것이 나음
# shapes_ocp.py
from abc import ABC, abstractmethod
from math import pi
class Shape(ABC):
def __init__(self, shape_type):
self.shape_type = shape_type
@abstractmethod
def calculate_area(self):
pass
class Circle(Shape):
def __init__(self, radius):
super().__init__("circle")
self.radius = radius
def calculate_area(self):
return pi * self.radius**2
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("rectangle")
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def calculate_area(self):
return self.side**2
Liskov subsituition principle (리스코프 치환 원칙)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀수 있어야한다.
- 상속받은 객체인 자식 객체는 부모 객체를 완전히 대체해도 아무런 문제가 없어야한다.
- 직사각형이라는 class가 있고, 정사각형이라는 class를 직사각형 부모class로 상속받아 만든다면
# shapes_lsp.py
# ...
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def __setattr__(self, key, value):
super().__setattr__(key, value)
if key in ("width", "height"):
self.__dict__["width"] = value
self.__dict__["height"] = value
>>> from shapes_lsp import Square
>>> square = Square(5)
>>> vars(square)
{'width': 5, 'height': 5}
>>> square.width = 7
>>> vars(square)
{'width': 7, 'height': 7}
>>> square.height = 9
>>> vars(square)
{'width': 9, 'height': 9}
- 정사각형의 넓이를 구하는 area 메소드를 overiding하였다.
- 정사각형의 넓이는 제대로 연산되나, 정사각형 class에서 다시 선언한 method인 area 를 이용해 직사각형을 다시 구하려면?! 제대로된 값이 구해지지 않을 것
- 리스코프 치환 원칙을 고려해서 class를 구성하려면 아래와 같이 구성하면 됨
- 부모 객체의 메서드를 그 의도에 맞지 않게 오버라이딩하는 것을 주의하자!
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def calculate_area(self):
return self.side ** 2
Interface segregation principle (인터페이스 분리 원칙)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 구현할 객체가 무의미한 메소드 구현을 방지하기 위해 각 class는 필요한 메서드만 가지고 있게 해야함 → 규모를 작게.
- 만약 Printer라는 class를 만들었는데, 그안에 많은 method를 가지고 있음
- 오래된 프린터는 fax, scan과 같은 기능이 필요없음에도, 추상클래스로 선언되어 가지고 있기때문에 불필요하게 오버라이딩으로 처리해야하는 문제가 있음
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print(self, document):
pass
@abstractmethod
def fax(self, document):
pass
@abstractmethod
def scan(self, document):
pass
class OldPrinter(Printer):
def print(self, document):
print(f"Printing {document} in black and white...")
def fax(self, document):
raise NotImplementedError("Fax functionality not supported")
def scan(self, document):
raise NotImplementedError("Scan functionality not supported")
class ModernPrinter(Printer):
def print(self, document):
print(f"Printing {document} in color...")
def fax(self, document):
print(f"Faxing {document}...")
def scan(self, document):
print(f"Scanning {document}...")
- 이러한 문제를 해결하기 위해 class를 여러개로 분리
- 기능별 print, fax, scan을 class로 분리해 놓으면 필요한 것만 상속받아 가져다 쓰면 됨
# printers_isp.py
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print(self, document):
pass
class Fax(ABC):
@abstractmethod
def fax(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan(self, document):
pass
class OldPrinter(Printer):
def print(self, document):
print(f"Printing {document} in black and white...")
class NewPrinter(Printer, Fax, Scanner):
def print(self, document):
print(f"Printing {document} in color...")
def fax(self, document):
print(f"Faxing {document}...")
def scan(self, document):
print(f"Scanning {document}...")
Dependency inversion principe (의존관계 역전 원칙)
- 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
- 아래와 같은 데이터를 화면에 뿌려주는 frontend를 구성했다고 하면
# app_dip.py
class FrontEnd:
def __init__(self, back_end):
self.back_end = back_end
def display_data(self):
data = self.back_end.get_data_from_database()
print("Display data:", data)
class BackEnd:
def get_data_from_database(self):
return "Data from the database"
그런데, 데이터를 API로 받고 싶어…
FrontEnd class의 back_end에 대한 수정이 필요해짐
하지만 이는 개방-폐쇄원칙에 위배됨
- 다른 방법
data source라는 호환이 가능한 class를 여러개 만들어 두고 상속받아 사용!!!
# app_dip.py
from abc import ABC, abstractmethod
class FrontEnd:
def __init__(self, data_source):
self.data_source = data_source
def display_data(self):
data = self.data_source.get_data()
print("Display data:", data)
class DataSource(ABC):
@abstractmethod
def get_data(self):
pass
class Database(DataSource):
def get_data(self):
return "Data from the database"
class API(DataSource):
def get_data(self):
return "Data from the API"
>>> from app_dip import API, Database, FrontEnd
>>> db_front_end = FrontEnd(Database())
>>> db_front_end.display_data()
Display data: Data from the database
>>> api_front_end = FrontEnd(API())
>>> api_front_end.display_data()
Display data: Data from the API
참조
'Python' 카테고리의 다른 글
어트리뷰트 (Attribute), 프로퍼티 (Property), 디스크립터 (Descriptor) (0) | 2023.08.30 |
---|---|
부동소수점(0.1+0.2 != 0.3 ???) (0) | 2023.07.17 |
* VS ** 차이 (리스트 or 딕셔너리 풀어낼때) (0) | 2023.05.05 |
[파이썬 클린코드] Chapter1. 코드 포매팅과 도구 (0) | 2023.01.15 |
[pandas] apply적용 (func return값이 multi일때 어떻게 적용?) (0) | 2022.04.27 |