TIL_29 : 다형성

JaHyeon Gu·2021년 8월 26일
0

OOP

목록 보기
4/5
post-thumbnail

🙄 다형성


➡ 다형성(Polymorphism)이란?

  • 하나의 변수가 서로 다른 클래스의 인스턴스를 가리킬 수 있는 성질
from math import pi


class Rectangle:
    """직사각형 클래스"""
    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


class Circle:
    """원 클래스"""
    def __init__(self, radius):
        self.radius = radius

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

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


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

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

    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])


rectangle = Rectangle(3, 7)
circle = Circle(4)

paint_program = Paint()
paint_program.add_shape(rectangle)
paint_program.add_shape(circle)

print(paint_program.total_perimeter_of_shapes())		# 45.132741228718345
print(paint_program.total_area_of_shapes())			# 71.26548245743669

  • shape 인스턴스가 없음에도 shape.area, shape.perimeter 가 정상 작동
  • 두 클래스가 area, perimeter 메소드를 갖고 있기 때문에 가능
  • 아래와 같이 원통 클래스를 추가하면 에러가 난다.
    👉 메소드 perimeter, area 가 없기 때문
class Cylinder:
    """원통 클래스"""
    def __init__(self, radius, height):
        self.radius = radius
        self.height = height
     
     
paint_program = Paint()

cylinder = Cylinder(7, 4)
rectangle = Rectangle(3, 7)
circle = Circle(4)

paint_program.add_shape(cylinder)
paint_program.add_shape(circle)
paint_program.add_shape(rectangle)

print(paint_program.total_perimeter_of_shapes())
print(paint_program.total_area_of_shapes())

# AttributeError: 'Cylinder' object has no attribute 'perimeter'

  • Paint 클래스에 아래와 같이 조건문을 추가해주면 정상 작동
    👉 그러나 앞으로 추가될 모든 도형에 대해 조건문을 수정해줘야하는 비효율 발생
    👉 더 안전하고 효율적인 다형성 적용 방법을 알아보자
class Paint:
    def __init__(self):
        self.shapes = []

    def add_shape(self, shape):
    	# 조건문 추가
        if isinstance(shape, Circle) or isinstance(shape, Rectangle):
            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])

➡ 상속을 활용한 다형성 #1

  • 상속을 활용하면 조건문에 isinstance()를 한개만 써줘도 됨
from math import pi


class Shape:
	"""공통 부모 클래스"""
    def area(self):
        """도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
        pass

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


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


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return pi * self.radius ** 2

    def perimeter(self):
        return 2 * pi * self.radius


class Cylinder:
    def __init__(self, radius, height):
        self.radius = radius
        self.height = 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])


cylinder = Cylinder(7, 4)
rectangle = Rectangle(3, 7)
circle = Circle(4)

paint_program = Paint()
paint_program.add_shape(cylinder)
paint_program.add_shape(circle)
paint_program.add_shape(rectangle)

print(paint_program.total_perimeter_of_shapes())
print(paint_program.total_area_of_shapes())

# 넓이, 둘레를 구하는 메소드가 없는 도형은 추가할 수 없습니다.
# 45.132741228718345
# 71.26548245743669

➡ 상속을 활용한 다형성 #2 : 문제점

  • area, perimeter 메소드가 자식 클래스에 무조건 오버라이딩 되어야 함
  • Shape 클래스에는 두 메소드가 pass기 때문에 아무것도 실행되지 않음
    👉 area, perimeter 메소드를 무조건 오버라이딩하도록 강제해보자
from math import pi


class Shape:
    def area(self):
        pass

    def perimeter(self):
        pass


class EquilateralTriangle(Shape):
    """정삼각형 클래스"""
    def __init__(self, side):		
        self.side = side	# Shape 클래스를 상속받지만 area, perimeter 메소드가 없음


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])


triangle = EquilateralTriangle(4)

paint_program = Paint()
paint_program.add_shape(triangle)

print(paint_program.total_perimeter_of_shapes())
print(paint_program.total_area_of_shapes())

# TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

➡ 상속을 활용한 다형성 #3 : 추상 클래스

  • 강제로 오버라이딩하게 만들기 위해서 추상 클래스를 사용
  • 추상 클래스란? : 여러 클래스들의 공통점을 추상화해서 모아놓은 클래스
  • 추상 클래스가 되기 위해선 추상 메소드가 필요
  • 추상 메소드란? : 자식 클래스가 무조건 오버라이딩 해야하는 메소드
  • decoratorabstractmethod를 갖는 메소드가 추상 메소드
  • 추상 클래스 만들기
    1. 모듈 abc로부터 클래스 ABC와 메소드 abstractmethodimport 한다
    2. 부모 클래스가 ABC 클래스를 상속받는다
    3. 부모 클래스의 메소드에 decoratorabstractmethod를 써준다
  • 추상 메소드가 최소 1개 이상 있어야 추상 클래스라고 할 수 있다
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass
 
 
shape = Shape()

# TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
  • 클래스 Shape자식 클래스는 반드시 두 메소드를 오버라이딩 해야 함
  • 추상 클래스로는 인스턴스를 만들 수 없다
  • 추상 클래스는 인스턴스를 직접 생성하기 위함이 아닌, 여러 클래스들의 공통점을 담아두고 다른 클래스들이 상속받는 부모 클래스가 될 목적으로 존재

➡ 상속을 활용한 다형성 #2 : 추상 클래스 활용

from math import pi, sqrt       	# sqrt : 제곱근 구하는 함수
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass


class EquilateralTriangle(Shape):
    """정삼각형 클래스"""
    def __init__(self, side):
        self.side = side
        
        
triangle = EquilateralTriangle(4)

paint_program = Paint()
paint_program.add_shape(triangle)

print(paint_program.total_perimeter_of_shapes())
print(paint_program.total_area_of_shapes())

# TypeError: Can't instantiate abstract class EquilateralTriangle with abstract methods area, perimeter
  • 정삼각형 클래스는 현재 추상 클래스
    👉 클래스 ABC를 상속받고 추상 메소드도 오버라이딩 없이 그대로 상속받은 상태
  • 오버라이딩함으로써 일반 메소드, 일반 클래스로 변경

from math import pi, sqrt       # 제곱근 구하는 함수
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

    @abstractmethod
    def perimeter(self) -> float:
        pass


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
        
        
triangle = EquilateralTriangle(4)

paint_program = Paint()
paint_program.add_shape(triangle)

print(paint_program.total_perimeter_of_shapes())
print(paint_program.total_area_of_shapes())

# 12
# 6.928203230275509
  • 오버라이딩의 방향을 제시하기 위해 추상 메소드는 type hinting을 해주는 게 좋음
  • 추상 클래스에도 일반 메소드 추가 가능
    👉 추상 메소드 : 반드시 오버라이딩
    👉 일반 메소드 : 오버라이딩할지 그대로 사용할지 자식 클래스에서 결정

➡ 자식 클래스 변수

  • 추상 메소드를 통해 자식 클래스가 해당 메소드를 갖도록 강제
  • 메소드뿐만 아니라 특정 변수를 갖도록 유도할 수 있음
from math import pi, sqrt
from abc import ABC, abstractmethod


class Shape(ABC):
    def area(self):
        pass

    def perimeter(self):
        pass

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

    @property       		# 순서가 뒤바뀌면 안됨
    @abstractmethod
    def y(self):        	# getter 메소드이자 추상 메소드
        """도형의 y 좌표 getter 메소드"""
        pass


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, value):
        """_x setter 메소드"""
        self._x = value

    @property
    def y(self):
        """_y getter 메소드"""
        return self._y

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


equilateral_triangle = EquilateralTriangle(5, 6, 4)
equilateral_triangle.x = 10
print(equilateral_triangle.x)		# 10

➡ 추상 클래스 다중 상속

  • 일반 클래스의 경우 다중 상속은 되도록 하지 않거나 하더라도 주의 필요
  • 추상 클래스의 경우 다중 상속받는 것은 일반적
  • 상속받는 추상 클래스들의 모든 추상 메소드를 오버라이딩해야 하기에 부모 추상 클래스들에 같은 이름의 추상 메소드들이 있다해도 오버라이딩한 내용이 반영됨

하지만 추상 클래스 속 일반 메소드가 겹친다면 문제 발생

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("이메일 내용입니다:\n{}".format(self.content))

    def send(self, destination):
         print("이메일 주소 {}에서 {}로 보냅니다!".format(self.user_email, destination))


email = Email("안녕? 오랜만이야 잘 지내니?", "jahyeon@gmail.com")
email.print_message()
email.send("goldstlme@gmail.com")

# 이메일 내용입니다:
# 안녕? 오랜만이야 잘 지내니?
# 이메일 주소 jahyeon@gmail.com에서 goldstlme@gmail.com로 보냅니다!



🙄 함수/메소드 다형성


  • 객체 지향 프로그래밍에서 다형성은 보통 클래스 다형성이지만 함수/메소드 다형성도 있음

➡ 옵셔널(Optional) 파라미터

  • 기본 값을 미리 지정해준 파라미터
  • 옵셔널 파라미터는 항상 파라미터 중 가장 뒤에 정의
def new_print(value_1, value_2 = None, value_3 = None):
    if value_3 is None:
        if value_2 is None:
            print(value_1)
        else:
            print("{} {}".format(value_1,value_2))
    else:
        print("{} {} {}".format(value_1, value_2, value_3))

new_print("this")		# this
new_print(0.5678)		# 0.5678
new_print("this", "that")	# this that
new_print("this", "that", 3)	# this that 3

➡ 파라미터 이름 명시

  • 함수를 호출할 때 파라미터 이름 표시
def print_name(first_name, last_name, email=""):
    print("{}{} {}".format(last_name, first_name, email))

print_name("자현", "구", "jahyeongu@gmail.com")
print_name(first_name="자현", last_name="구",email="jahyeongu@gmail.com")
print_name(last_name="구",email="jahyeongu@gmail.com", first_name="자현")
print_name(last_name="구", first_name="자현")

# 구자현 jahyeongu@gmail.com
# 구자현 jahyeongu@gmail.com
# 구자현 jahyeongu@gmail.com
# 구자현

➡ 개수가 확정되지 않은 파라미터

  • 마지막 파라미터 이름 앞에 *
def print_message_and_add_numbers(message, *numbers):
    print(message)
    return sum(numbers)

print(print_message_and_add_numbers("test1", 7, 3, 5))
print(print_message_and_add_numbers("test2", 1, 2, 3, 4))
print(print_message_and_add_numbers("test3", 2, 4, 6, 8, 10))

# test1
# 15
# test2
# 10
# test3
# 30



🙄 코딩 스타일: LBYL vs EAFP


  • LBYL (Look Before You Leap)
  • 어떤 작업을 수행하기 전에 그 작업을 수행해도 괜찮을지 확인
class Paint:
    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])

  • EAFP (Easier to Ask for Forgiveness than Permission)
  • 일단 먼저 빨리 실행하고, 문제가 생기면 처리
    def add_shape(self, shape):
        self.shapes.append(shape)


    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))
profile
IWBAGDS

0개의 댓글

관련 채용 정보