저번 시간에는 객체 지향 프로그래밍의 마지막 기둥, 다형성의 개념과 상속을 활용한 다형성 적용 방법, 추상 클래스의 활용 등에 대해 함께 알아봤습니다.

이번 시간에는 다형성의 더 다양한 활용법을 배워봅시다.

🤹‍♂️ 추상 클래스 더 알아보기

Python에서 사용하는 추상 클래스에 관해 알아야 할 4가지가 있습니다. 함께 알아봅시다.

❗ 추상 클래스와 추상화

추상 클래스라는 말을 들으니 우리가 지난 시간에 배웠던 추상화라는 개념이 떠오릅니다. 추상화는 변수, 함수, 클래스를 사용해 사용자가 꼭 알아야 하는 부분만 겉으로 드러내는 것이라고 배웠었죠? 사실 이번에 배운 추상 클래스도 이러한 추상화의 한 예입니다.

추상 클래스는 서로 관련 있는 클래스들의 공통 부분을 묶어서 추상화합니다. 이해를 돕기 위해 그림판 프로그램으로 다시 돌아가 볼까요?

Paint 클래스를 사용하는 개발자는 add_shape 메소드에서 파라미터 shape로 들어오는 인스턴스의 자료형이 Shape 클래스일 때만 그 인스턴스를 추가합니다. 이때, 해당 인스턴스가 구체적으로 어떤 도형의 인스턴스인지관심이 없습니다.

여기서 Shape 클래스는 추상 클래스입니다. Shape 클래스의 인스턴스라는 것은 그 인스턴스의 클래스가 Shape 클래스를 상속받은 자식 클래스라는 뜻입니다. 또한, 해당 클래스는 추상 메소드 area와 perimeter를 오버라이딩한 클래스이기도 합니다.

정리하자면, 도형을 나타내는 클래스라면 가질 수 밖에 없는 공통점을 Shape 클래스 하나로 추상화한 것이 됩니다.

이렇게 하면 Paint 클래스 코드의 개발자는 추상 클래스로 추상화된 수준(Shape 클래스)까지만 생각하고 개발을 진행할 수 있습니다. 이 말은 즉, 개발자가 추가된 각 도형 인스턴스의 출처를 일일히 알 필요없이, 일단 두 공통 메소드를 가지는 인스턴스라고 생각하고 개발할 수 있다는 뜻이죠.

하나만 더 추가하자면, add_shape 메소드에 Shape 타입을 가지는 인스턴스가 shape 파라미터로 들어와야 한다는 것을 알려주기 위해 type hinting 기능을 사용합니다(type hinting은 추상화를 하는 방법 중 하나였죠?).

def add_shape(self, shape: Shape):

이것만으로는 Shape 클래스의 인스턴스만 들어오도록 강제할 수는 없지만 이런 식으로 정보를 남겨주어야 개발자가 Paint 클래스를 제대로 사용할 수 있게끔 할 수 있습니다.

❗ 추상 클래스에 일반 메소드 추가하기

추상 클래스에 반드시 추상 메소드만 있어야 하는 것은 아닙니다. 이 말은 곧, @abstractmethod 데코레이터가 없는 일반적인 메소드가 있어도 상관 없다는 뜻이죠. 이 메소드들 또한 자식 클래스가 물려받아 그대로 사용하거나 오버라이딩 할 수 있습니다.

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

larger_than 메소드는 @abstractmethod 데코레이터가 없는 일반 메소드입니다. 이 메소드는 파라미터로 전달된 다른 도형 인스턴스의 넓이와 자신의 넓이를 비교합니다.

Circle 클래스에 이 메소드를 호출해보면,

circle = Circle(5)
rectangle = Rectangle(1, 2)
print(circle.larger_than(rectangle))
True

정상적으로 작동합니다.

두 메소드 간의 차이점이 있다면, 반드시 오버라이딩 해야하는 추상 메소드와 달리 일반 메소드는 물려받은 그대로 사용할지, 오버라이딩해서 사용할지를 자식 클래스에서 결정할 수 있습니다.

❗ 추상 메소드에 내용 추가하기

지금까지 추상 메소드에는 pass만 담겨있었습니다. 그러나 추상 메소드에도 내용을 담을 수 있습니다.

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

그런데 좀 어색합니다. 어차피 자식 클래스에서 추상 메소드들을 반드시 오버라이딩 해야하기 때문에 추상 메소드의 내용은 무시되기 쉽습니다.

하지만 경우에 따라 이 내용이 유용할 때가 있습니다. 보통 추상 메소드에 내용을 적을 때는 모든 자식 클래스에 해당하는 공통 내용을 써줍니다. 그리고 자식 클래스에서 추상 메소드를 오버라이딩 하더라도 미리 채워진 내용을 가져와서 재활용하는 것이 가능합니다. super 함수를 쓰면 말이죠.

def area(self):
    """직사각형의 넓이를 리턴한다"""
    super().area()
    return self.width * self.height

앞서 부모 클래스의 이닛 메소드를 사용할 때, 자식 클래스에서 super 함수를 통해 부모 클래스의 내용에 접근할 수 있다고 배웠습니다. 이렇게 하면 부모 클래스 Shape에서 area 메소드를 실행할 수 있습니다.

즉, 물려 받은 추상 메소드를 오버라이딩 하는데 super 함수를 통해 추상 메소드의 기존 내용(print 함수)을 포함함과 동시에 별도로 자신만의 내용(return 아래)을 추가한 것이죠.

이렇게 모든 자식 클래스에서 공통적으로 사용할 부분을 추상 메소드의 내용으로 써주고 super 함수를 통해 자식 클래스에서 접근하는 방법은 꽤 자주 쓰입니다.

❗ 자식 클래스가 특정 변수 갖도록 유도하기

추상 클래스를 사용하면 자식 클래스가 추상 클래스의 추상 메소드를 오버라이딩하도록 즉, 해당 메소드를 갖도록 강제할 수 있습니다. 이 밖에도 추상 클래스로 자식 클래스가 특정 변수를 갖도록 유도할 수 있는 방법도 있습니다.

그림판에서 사용할 모든 도형 클래스는 좌표를 나타내는 인스턴스 변수 x와 y를 무조건 가져야한다고 가정합시다. 어떻게 하면 추상 클래스를 사용해 각 자식 클래스가 이 변수를 갖도록 유도할 수 있을까요?

@property
@abstractmethod
def x(self):
    """도형의 x 좌표 getter 메소드"""
    pass

@property
@abstractmethod
def y(self):
    """도형의 y 좌표 getter 메소드"""
    pass

위 코드는 Shape 클래스가 x 메소드와 y 메소드를 getter 메소드이자 추상 메소드로 가지고 있는 모습을 보여주고 있습니다. @property@abstractmethod 데코레이터를 연달아 적어주면 해당 메소드는 getter 메소드이자 추상 메소드가 됩니다. 즉, 어떤 변수에 대한 getter 메소드를 뜻하지만 아직 내용이 비어있어 어떤 변수를 리턴하는지는 결정되지 않은 것이죠.

이때, Shape 클래스를 상속 받는 자식 클래스에서 어떤 변수를 리턴하는지 즉, 이 getter 메소드가 어떤 변수에 대한 것인지를 나타내도록 오버라이딩 해야합니다.

앞서 정의했던 정삼각형 클래스를 다시 봅시다.

class EquilateralTriangle(Shape):
    """정삼각형을 나타내는 클래스"""
    def __init__(self, side):
        self.side = side
    
    def area(self):
        """정삼각형의 넓이를 리턴한다"""
        return sqrt(3) * self.side * self.side / 4
    
    def perimeter(self):
        """정삼각형의 둘레를 리턴한다"""
        return 3 * self.side
equilateral_triangle = EquilateralTriangle(3)

이때, getter 메소드들을 오버라이딩 하지 않으면 다음과 같은 에러가 발생합니다.

TypeError: Can't instantiate abstract class EquilateralTrianlge with abstract methods x, y

이는 추상 메소드 x, y를 오버라이딩 하지 않아서 생긴 에러인데요. 그렇다면 각 getter 메소드를 어떻게 오버라이딩 하면 될까요?

캡슐화에서 인스턴스 변수의 이름을 _cat과 같이 나타내고 getter 메소드의 이름은 cat과 같이 변수 이름 앞에서 밑줄을 뺀 이름으로 한다고 배웠습니다.

지금 이 내용을 적용한다면 x는 인스턴스 변수 _x의 getter 메소드로, y는 인스턴스 변수 _y의 getter 메소드로 해주면 좋을 것 같네요.

@property
def x(self):
    """_x getter 메소드"""
    return self._x

@x.setter
def x(self):
    """_x setter 메소드"""
    self.x = value

혹시나 @property 데코레이터의 기능이 잘 생각나지 않는 분을 위해 설명하자면, 이 코드는 클래스의 인스턴스에 대해 self.x, 인스턴스 이름.x와 같은 부분을 실행할 때, getter 메소드 x를 실행한다는 의미를 가지고 있습니다. 다시 말해, @property가 붙으면 이런 구문들이 인스턴스 변수 x의 값을 직접 읽는다는 원래 뜻이 아닌 getter 메소드 x를 실행한다는 의미로 바뀝니다.

아래의 @x.setter가 붙은 메소드는 클래스의 인스턴스에 대해 self.x =3, 인스턴스 이름.x = 3과 같은 부분을 실행할 때 setter 메소드 x를 실행한다는 의미입니다. 다시 말해, @x.setter가 붙으면 이런 구문들이 인스턴스 변수 x에 어떤 값을 설정한다는 원래 뜻이 아닌 setter 메소드 x를 실행한다는 의미로 바뀌는 것이구요.

그럼 완성된 정삼각형 클래스의 전체 코드를 봅시다.

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
        
    @property
    def x(self):
        """_x getter 메소드"""
        return self._x

    @x.setter
    def x(self):
        """_x setter 메소드"""
        self.x = value
         
    @property
    def y(self):
        """_y getter 메소드"""
        return self._y

    @y.setter
    def y(self):
        """_y setter 메소드"""
        self.y = value       

잘 실행되는지 테스트 해보겠습니다.

equilateral_triangle = EquilateralTriangle(5, 6, 4)

equilateral_triangle.x = 20
print(equilateral_triangle.x)

equilateral_triangle.y = 10
print(equilateral_triangle.y)
20
10

문제 없이 잘 실행됩니다.

물론 Shape 클래스에서 자식 클래스에 getter 메소드를 오버라이딩 하도록 강제한다고 해도 자식 클래스에서 이 메소드로 하여금 변수의 내용을 가져오라는 getter 메소드로서의 내용이 아닌 아예 엉뚱한 내용으로 오버라이딩할 수도 있습니다. 하지만 Python의 문화를 잘 따르는 개발자라면 getter/setter 메소드의 내용이 되도록 오버라이딩할 것입니다.

이렇게 하면 부모 클래스에서 추상 메소드인 getter 메소드를 만들어서 자식 클래스가 그 getter 메소드의 대상이 되는 인스턴스 변수를 갖도록 유도할 수 있습니다.

🤹‍♂️ 추상 클래스 다중 상속 받기

저번 시간에 다중 상속에 대해 배웠었는데요. 다중 상속은 되도록 하지 않거나 불가피하게 쓰게 되더라도 주의해서 사용해야 한다고 했습니다. 그러나 추상 클래스에서는 이러한 주의가 덜 요구되기 때문에 일반적으로 추상 클래스 여러 개를 다중 상속 받는 일이 많습니다.

from abc import ABC, abstractmethod

class Message(ABC):
    @abstractmethod
    def print_message(self) -> None:
        pass
        
class Sendable(ABC):
    @abstractmethod
    def send(self, destination: str) -> None:
        pass
        
class Email(Message, Sendable):
    def __init__(self, content, user_email):
        self.content = content
        self.user_email = user_email
        
    def print_message(self):
        print(f"이메일 내용입니다:\n{self.content}")
        
    def send(self, destination):
        print(f"이메일을 주소 {self.user_email}에서 {destination}(으)로 보냅니다!")

추상 클래스 Message와 Sendable를 다중 상속 받는 Email 클래스를 정의했습니다. 이 클래스는 각 추상 클래스의 추상 메소드들인 print_message와 send 메소드를 잘 오버라이딩 했네요.

이 클래스를 사용해볼까요?

email = Email("안녕! 잘 지내?", "tataki26@email.com")

email.print_message()
email.send("firedragon12@email.com)
이메일 내용입니다:
안녕? 잘 지내?
이메일을 주소 tataki26@email.com에서 firedragon12@email.com(으)로 보냅니다!

잘 동작하네요 :)

❗ 추상 메소드가 겹칠 때

그런데 왜 추상 클래스에서는 다중 상속 받는 것이 좀 더 일반적일까요? 예시로 한 번 알아보겠습니다.

class Message(ABC):
    @abstractmethod
    def print_message(self) -> None:
        pass
        
    @abstractmethod
    def send(self, destination: str) -> None:
        pass
        
class Sendable(ABC):
    @abstractmethod
    def send(self, destination: str) -> None:
        pass

Message 클래스와 Sendable 클래스는 send라는 공통된 메소드를 가지고 있습니다. 이렇게 두 추상 클래스 사이에 중복되는 메소드가 있어도 위 코드는 여전히 잘 동작합니다.

사실 잘 생각해보면 결과가 바뀔 이유가 딱히 없습니다. 자식 클래스는 어차피 상속 받는 추상 클래스들의 모든 추상 메소드를 오버라이딩해야 하기 때문에 아무리 부모 추상 클래스에 같은 이름의 추상 메소드가 있다고 해도 새롭게 오버라이딩한 내용이 반영됩니다. 따라서, 만약 부모 추상 클래스들 사이에 겹치는 메소드가 추상 메소드라면 문제 없이 다중 상속이 가능합니다.

그러나 일반 메소드가 겹치면 문제가 발생했었죠?

class Message(ABC):
    @abstractmethod
    def print_message(self) -> None:
        pass
        
    def __str__(self):
        return "Message 클래스의 인스턴스"
        
class Sendable(ABC):
    @abstractmethod
    def send(self, destination: str) -> None:
        pass
        
    def __str__(self):
        return "Message 클래스의 인스턴스"

위 코드와 같이 부모 추상 클래스 간에 일반 메소드가 겹친다면 일반 클래스를 다중 상속할 때와 마찬가지로 주의해야 합니다. 자식 클래스에서 일반 메소드를 오버라이딩하지 않으면 어느 추상 클래스의 일반 메소드를 실행해야 하는지 알 수 없기 때문입니다. 물론 mro라는 규칙으로 해결할 수 있긴 하지만요.

정리하자면,

  1. 추상 클래스 다중 상속은 일반적으로 많이 사용한다
  2. 다중 상속 받는 부모 추상 클래스들이 추상 메소드로만 이루어져 있으면 아무 문제 없이 다중 상속 받을 수 있다
  3. 다중 상속 받는 부모 추상 클래스들 간에 이름이 겹치는 일반 메소드가 있으면 일반 클래스를 다중 상속 받을 때와 동일한 문제가 발생할 수 있다

이 세 가지를 기억해주세요!

🤹‍♂️ 함수/메소드 다형성

지금까지 우리가 배운 것은 '클래스' 다형성입니다. 객체 지향 프로그래밍에서 다형성은 보통 이 클래스 다형성을 말합니다. 하지만 다른 종류의 다형성도 존재하는데요. 바로 함수나 메소드의 다형성입니다.

print("hey")
print(0.12)
print("hey", "hi")
print("hey", 0.12)
print("hey", sep="")

위 코드와 같이 여러 가지 형태로 함수나 메소드를 호출하는 것을 함수나 메소드의 다형성이라고 합니다.

예시를 통해 조금 더 살펴보도록 하겠습니다.

첫번째 예시옵셔널(optional) 파라미터입니다. 옵셔널 파라미터란, 기본 값을 미리 지정해준 파라미터를 말합니다.

def new_print(value1, value2=None, value3=None):
    if value3 is None:
        if value2 is None:
            print(value1)
        else:
            print(f"{value1} {value2}")
     else:
         print(f"{value1} {value2} {value3}")
         
new_print("hey")
new_print("0.12")
new_print("hey", "hi")
new_print("hey", 0.12)

new_print의 두번째 파라미터와 세번째 파라미터는 모두 기본값이 None입니다. Python에서 None아무 값도 없다는 뜻으로 함수 호출 시 어떤 파라미터에 값을 전달하지 않은 경우를 나타냅니다. 이렇게 하면 두번째, 세번째 파라미터에 값을 전달하지 않아도 자동으로 None이 됩니다.

주의할 점은 옵셔널 파라미터는 파라미터 중 가장 뒤에 위치해야 한다는 것입니다. 만약 자리를 바꾸면 어떻게 될까요? 함수 호출 부분에서 어떤 파라미터를 가져와야 할 지 알 수가 없습니다.

이렇듯 옵셔널 파라미터 뒤에 일반 파라미터가 나오면 Python이 메소드를 제대로 호출할 수 없습니다. 그래서 항상 맨 뒤에 몰아 쓰는 것이죠.

두번째 예시는 함수를 호출할 때 파라미터 이름을 표시하는 것입니다.

def print_name(first_name, last_name, email="")
    print(f"{last_name}{first_name} {email})
    
print_name("탸키", "타키", "tataki26@email.com")
print_name(first_name="탸키", last_name="타키", email="tataki26@email.com")
print(email="tataki26@email.com", first_name="탸키", last_name="타키")
print_name(first_name="탸키", last_name="타키")

두번째와 세번째 print 함수는 아예 파라미터 이름을 적고 등호와 값을 적어주고 있습니다. 어느 파라미터에 어느 값을 넣을지 확실히 표기한 것이죠. 심지어 순서를 바꿔도 문제 없는데요. 이는 파라미터 이름을 분명히 표현했기 때문이죠.

print_name 함수에는 email 파라미터가 옵셔널 파라미터입니다. 따라서 마지막 코드처럼 두 개의 파라미터만 넘겨주더라도 잘 호출됩니다.

세번째 예시는 파라미터에 별표(*)가 붙은 경우입니다. 개수가 정해지지 않은 파라미터를 받아야 할 때, 마지막 파라미터 앞에 별표를 써줍니다.

def print_message_and_add_nums(message, *nums):
    print(message)
    return sum(nums)

위 함수에는 마지막 파라미터 nums에 별표가 붙어있습니다. 이렇게 쓰면 첫번째 파라미터로 message가 넘어가고 나머지 값들은 모두 nums라는 튜플에 담겨서 전달됩니다.

이 함수는 nums에 있는 모든 요소의 합을 리턴하는데요.

print(print_message_and_add_nums("test1", 3, 6, 8))
print(print_message_and_add_nums("test2", 1, 3, 5, 7))
print(print_message_and_add_nums("test3", 2))

위 코드를 실행하면 test1만 첫번째 파라미터에 담기고 나머지 값들은 nums라는 튜플에 담깁니다.

test1
17
test2
16
test3
2

이때, 만약 nums가 첫번째 파라미터로 나오면 어떻게 될까요? 두 파라미터의 순서를 바꾸면 에러가 납니다. 이를 해결하려면 별표 파라미터 뒤에 있는 파라미터의 이름을 명시해야 합니다.

print(print_message_and_add_nums(3, 6, 8, message="test1"))
print(print_message_and_add_nums(1, 3, 5, 7, message="test2"))
print(print_message_and_add_nums(2, message="test3"))

위 코드를 실행하면 정상적으로 동일한 결과가 나옵니다.

함수를 클래스 안으로 옮기면 메소드 다형성도 가능합니다. 보통은 클래스 다형성이지만 이렇게 함수/메소드 다형성도 있다는 점을 기억하시길 바랍니다.

🤹‍♂️ Python EAFP 코딩 스타일

지금까지 완성해온 코드들을 정리해봅시다. 추상 클래스인 Shape가 있고 그 아래로 직사각형, 원통, 정삼각형, 직각삼각형 같은 여러 도형 클래스와 그림판을 나타내는 Paint 클래스가 있었습니다.

Paint 클래스에는 add_shape라는 메소드가 있었죠? 이 메소드의 역할은 Shape 클래스의 인스턴스만 shapes 리스트에 추가하고 그 외에는 다른 도형을 입력해달라는 메시지를 전달하는 것이었습니다.

이때, Shape 클래스의 인스턴스인지 확인하는 과정확인 작업이라고 하는데요. 이렇게 확인 작업을 하며 코딩하는 스타일LBYL이라고 합니다. 이는 'Look Before You Leap'라는 말의 약어로 우리말로는 '뛰기 전에 살펴보라'는 뜻을 가지고 있습니다. 돌다리도 두드려보고 건너라는 말이 있죠? 이처럼 LBYL은 어떤 작업을 수행하기 전에 그 작업을 수행해도 괜찮을지 확인하라는 말입니다.

이와 반대로, Python에는 'Easier to Ask for Forgiveness than Permission'의 약어인 EAFP, 우리말로는 '허락보다 용서를 구하는 것이 쉽다'라는 뜻을 가진 코딩 스타일도 있습니다. 이는 일단 먼저 실행하고 문제가 생기면 처리하라는 의미를 가지고 있습니다.

LBYL 스타일로 작성된 Paint 클래스를 EAFP 스타일로 바꿔서 작성해보겠습니다.

def add_shape(self, shape):
    """그림판에 도형을 추가한다"""
    if isinstance(shape, Shape):
        self.shapes.append(shape)
    else:
        print("넓이, 둘레를 구하는 메소드가 없는 도형은 추가할 수 없습니다!")

위 코드를,

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

이렇게 바꿉니다. 확인 작업 코드를 없앴죠? 또 하나, shape이 Shape 클래스의 인스턴스여야 하는 사실을 type hinting을 통해 나타냈습니다. 그에 맞게 주석도 수정해줬구요. 다만, type hinting과 주석은 설명만 할 뿐, 특정 인스턴스만 들어오도록 강제하는 기능은 없습니다. 언제든 다른 인스턴스가 들어올 수 있다는 말이죠.

그럼 이와 같은 문제를 어떻게 막아야 할까요? 우선, total_area_of_shapes 메소드의 코드를 수정해야 합니다.

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

위 코드를,

def total_area_of_shape(self):
    """그림판에 있는 모든 도형의 넓이와 합을 구한다"""
    total_area = 0
    
    for shape in self.shapes:
        try:
            total_area += shape.area()
        except (AttribueteError, TypeError):
            print(f"그림판에 area 메소드가 없거나 잘못 정의되어 있는 인스턴스 {shape}가 있습니다.")
    
    return total_area

이렇게 수정했습니다.

코드를 한 줄씩 설명해 드리겠습니다. 먼저, 넓이의 합을 담을 변수를 선언합니다. 다음으로 shape에 있는 도형을 차례로 더해야 합니다. 그런데 for문 안에 정의된 내용이 좀 낯설죠? try문에는 실행할 코드, 그러나 에러가 발생할 수 있는 코드를 넣습니다. 그러니까 try문 안에 있는 내용은 에러가 발생할 수 있는 코드인데요.

이는 앞서 삭제한 확인 작업 코드를 대신하는 부분으로, 확인 작업에서 해야할 일을 대신 맡아 처리하기 위해 존재합니다. 에러가 발생할 경우에는 아래 except를 통해 해결 방법을 알려줍니다. 즉, 명시된 AttributeError나 TypeError가 나타날 경우에는 except문의 코드가 실행됩니다.

다시 말해, try문은 에러를 예견하기 때문에 에러가 발생하더라도 프로그램이 멈추지 않고 except가 수행되도록 일을 넘겨줍니다. 이런 식으로 일단 수행하고 문제가 발생하면 처리하는 방식이 바로 EAFP 스타일입니다.

이어서 total_perimeter_of_shapes 메소드의 코드도 수정해보겠습니다.

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

이와 같은 코드를,

def total_perimeter_of_shapes(self):
    """그림판에 있는 모든 도형의 둘레의 합을 구한다"""
    total_perimeter = 0
    
    for shape in self.shapes:
        try:
            total_perimeter += shape.perimeter()
        except (AttributeError, TypeError):
            print(f"그림판에 perimeter 메소드가 없거나 잘못 정의되어 있는 인스턴스 {shape}가 있습니다.")
            
    return total_perimeter

이렇게 수정했습니다.

실행을 해보면 이전 코드와 다른 결과가 나오는데요!

그림판에 area 메소드가 없거나 잘못 정의되어 있는 인스턴스 밑면 반지름 4, 높이 3인 원기둥가 있습니다.
(넓이의 합: 생략)
그림판에 perimeter 메소드가 없거나 잘못 정의되어 있는 인스턴스 밑면 반지름 4, 높이 3인 원기둥가 있습니다.
(둘레의 합: 생략)

밑면 반지름 4, 높이 3인 원기둥
(나머지 도형들: 생략)

except문의 오류 메시지가 등장하고 원통의 내용도 출력됩니다. 이는 기존 코드에서 확인 작업에서 걸러졌던 원통이 확인 작업 코드가 사라지고 처리되는 과정에서 잔존했기 때문이고 try문에 와서야 비로소 에러를 발생시키면서 except 부분의 메시지를 출력해서 생긴 결과입니다.

이와 같은 EAFP 스타일은 Python 언어가 지향하는 방식입니다. 그렇다고 해서 꼭 Python의 모든 코드를 EAFP 방식으로 작성할 필요는 없습니다. 대신, 같은 코드도 여러 가지 스타일로 작성할 수 있다는 것을 보여주는 사례라고 생각하는 것이 좋겠네요.


지금까지 객체 지향 프로그래밍의 네 가지 기둥, 추상화, 캡슐화, 상속, 다형성을 모두 배웠습니다. 네 가지를 배우고 나니 객체 지향적 설계법이 잘 그려지는 것 같습니다. 새롭게 배운 내용이 많아 복습을 권장합니다.

다음 시간에는 객체 지향 코드를 견고하게 관리할 수 있는 방법에 대해 알아보도록 하겠습니다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글