회원가입

16. [다형성] 파이썬 EAFP 코딩 스타일과 다형성

NULL 2021-10-15

지금 코드에는 추상 클래스인 도형 클래스, 그리고 직사각형 클래스, 원 클래스, 원통 클래스, 정삼각형, 직각삼각형 같은 여러 도형 클래스와 그림판을 나타내는 Paint 클래스가 있다.

from math import sqrt, pi
from abc import ABC, abstractmethod

class Shape(ABC):
    """도형 클래스"""

    @abstractmethod
    def area(self) -> float:
        """도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
        pass

    @abstractmethod
    def perimeter(self) -> float:
        """도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
        pass

    def larger_than(self, shape):
        """해당 인스턴스의 넓이가 파라미터 인스턴스의 넓이보다 큰지를 불린으로 나타낸다"""
        return self.area() > shape.area()

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 + 2 * self.height

    def __str__(self):
        """직사각형의 정보를 문자열로 리턴한다"""
        return "밑변 {}, 높이 {}인 직사각형".format(self.width, self.height)

class Circle(Shape):
    """원 클래스"""

    def __init__(self, radius):
        self.radius = radius

    def area(self):
        """원의 넓이를 리턴한다"""
        return pi * self.radius * self.radius

    def perimeter(self):
        """원의 둘레를 리턴한다"""
        return 2 * pi * self.radius

    def __str__(self):
        """원의 정보를 문자열로 리턴한다"""
        return "반지름 {}인 원".format(self.radius)

class Cylinder:
    """원통 클래스"""

    def __init__(self, radius, height):
        self.radius = radius
        self.height = height

    def __str__(self):
        """원통의 정보를 문자열로 리턴하는 메소드"""

        return "밑면 반지름 {}, 높이 {}인 원기둥".format(self.radius, self.height)

class EquilateralTriangle(Shape):
    """정삼각형 클래스"""
    def __init__(self, x, y, side):
        self._x = x
        self._y = y
        self.side = side

    def area(self):
        """정삼각형의 넓이를 리턴한다"""
        return sqrt(3) * self.side * self.side / 4

    def perimeter(self):
        """정삼각형의 둘레를 리턴한다"""
        return 3 * self.side

    def __str__(self):
        """정삼각형의 정보를 문자열로 리턴하는 메서드"""
        return "한 변의 길이가 {}인 정삼각형".format(self.side)

class RightTriangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return self.base * self.height / 2

    def perimeter(self):
        return sqrt(self.base ** 2 + self.height ** 2) + self.base + self.height

    def __str__(self):
        """지각삼각형의 정보를 문자열로 리턴한다"""
        return "밑변 {}, 높이 {}인 직각삼각형".format(self.base, self.height)

class Paint:
    """그림판 프로그램 클래스"""
    def __init__(self):
        self.shapes = []

    def add_shape(self, shape):
        """도형 인스턴스만 그림판에 추가한다"""
        if isinstance(shape, Shape):
            self.shapes.append(shape)
        else:
            print("도형 클래스가 아닌 인스턴스는 추가할 수 없습니다!")

    def total_area_of_shapes(self):
        """그림판에 있는 모든 도형의 넓이의 합을 구한다"""
        return sum([shape.area() for shape in self.shapes])

    def total_perimeter_of_shapes(self):
        """그림판에 있는 모든 도형의 둘레의 합을 구한다"""
        return sum([shape.perimeter() for shape in self.shapes])

    def __str__(self):
        """그림판에 있는 각 도형들의 정보를 문자열로 리턴한다"""
        res_str = "그림판 안에 있는 도형들:\\n\\n"
        for shape in self.shapes:
            res_str += str(shape) + "\\n"
        return res_str

 

먼저 예전에 작성했던 add_shape 메서드를 보자

def add_shape(self, shape):
    """도형 인스턴스만 그림판에 추가한다"""
    if isinstance(shape, Shape):
        self.shapes.append(shape)
    else:
        print("도형 클래스가 아닌 인스턴스는 추가할 수 없습니다!")

add_shape 메서드Shape 클래스의 인스턴스shapes 리스트에 추가한다.

그런데 shapes 리스트에 인스턴스를 추가하기 전에 Shape 클래스의 인스턴스가 맞는지 확인하고 있다.

이렇게 어떤 작업 전에 확인을 거치는 코딩 스타일을 LBYL 이라고 한다.

 

LBYL (Look Before You Leap)


뛰기 전에 살펴보라는 뜻이다.

우리 말로는 "돌 다리도 두드려보고 건너라" 라는 말이다.

 

어떤 작업을 수행하기 전에 그 작업을 수행해도 괜찮을지 확인하는 것이다.

파이썬에서는 이런 LBYL 스타일과 정반대로 일단 실행하고 보는 EAFP 라는 코딩 스타일이 있다.

 

EAFP (Easier to Ask for Forgiveness than Permission)


허락보다 용서가 쉽다 라는 뜻이다.

일단 먼저 빨리 실행하고, 문제가 생기면 그때 처리하자는 식의 사고방식이다.

 

지금 Paint 클래스는 LBYL 스타일로 작성이되었다.

이것을 EAFP 스타일로 바꿔보자.

 

먼저 add_shape 메서드를 별다른 확인 작업 없이 도형을 바로 그림판에 추가하는 방식으로 바꿀 것이다.

def add_shape(self, shape):
    """도형 인스턴스만 그림판에 추가한다"""
    self.shapes.append(shape)

 

그 다음에는 이 shapeShape 클래스의 인스턴스여야 한다는 type hinting 을 추가하겠다.

def add_shape(self, shape: Shape):
    """도형 인스턴스만 그림판에 추가한다"""
    self.shapes.append(shape)

 

그리고 나서 주석도 바꿔줄 것이다.

def add_shape(self, shape: Shape):
    """
    그림판의 도형 인스턴스 shape 을 추가한다.
    단, shape 은 추상 클래스 Shape 의 인스턴스여야 한다
    """
    self.shapes.append(shape)

 

물론 이런 type hinting 과 주석은 설명을 위한 것일 뿐, 실제로 Shape 클래스의 인스턴스만 들어오도록 강제하지는 못한다.

그러니까 Shape 클래스의 인스턴스가 아닌 인스턴스들도 들어올 수 있다는 것이다.

 

이제 add_shape 메서드가 깔끔해진 대신 Shape 클래스의 인스턴스가 아닌 그러니까 area perimeter 메서드가 없는 인스턴스가 들어올 위험성도 생겼다.

이런 위험성을 대비하기 위해 total_area_of_shapes 메서드를 수정해야한다.

def total_area_of_shapes(self):
    """그림판에 있는 모든 도형의 넓이의 합을 구한다"""
    total_area = 0

    for shape in self.shapes:
        try:
            total_area += shape.area()
        except (AttributeError, TypeError):
            print("그림판에 area 메서드가 없거나 잘못 정의되어 있는 인스턴스 {}가 있습니다.".format(shape))

tryexcept 로 예외처리를 했다.

이런 방식이 EAFP 방식이다.

 

정리하자면 기존에 add_shape 메서드에서 번거롭게 Shape 클래스의 인스턴스인지 확인하는 부분을 뺐다.

그 대신 도형 인스턴스area 메서드를 호출하다가 에러를 발생할 때의 대비책을 마련해줬다.

그 다음에는 total_perimeter_of_shape 메서드도 이 EAFP 스타일로 바꿔주자.

def total_perimeter_of_shapes(self):
    """그림판에 있는 모든 도형의 둘레의 합을 구한다"""
    total_perimeter = 0

    for shape in self.shapes:
        try:
            total_perimeter += shape.perimeter()
        except (AttributeError, TypeError):
            print("그림판에 perimeter 메서드가 없거나 잘못 정의되어 있는 인스턴스 {}가 있습니다.".format(shape))

 

이런 방식 Easier to Ask for Forgiveness than Permission 파이썬스러운 스타일이라고 한다.

물론 항상 EAFP 스타일로만 파이썬 코드를 써야 하는 것은 아니다.

이렇게 같은 동작을 하더라도 스타일을 다르게 해서 쓸 수 있다는 것을 기억하자.

1 0