* 이 글은 코드잇의 <객체 지향 프로그래밍> 코스를 수강하고 정리한 글입니다.
* 나중에라도 제가 참고하기 위해 정리해 두었으며, 모든 내용을 적은 것이 아닌,
필요하다고 생각되는 부분만 추려서 정리한 것임을 미리 밝힙니다.
다형성이란?
다형성의 원래 정의: 여러가지의 형태를 갖는 성질
객체 지향 프로그래밍에서의 다형성: 하나의 변수가 서로 다른 클래스의 인스턴스를 가리킬 수 있는 성질
예시Circle
이라는 클래스와 Rectangle
이라는 클래스가 있을 때, 각각의 클래스가 모두 넓이를 계산하는 area
라는 메소드를 가지고 있다고 해보자. 한편, 추가된 모든 도형의 넓이 총합을 계산해주는 클래스가 다음과 같이 있다고 할 때,
class Canvas:
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])
일단 다음과 같이 네모 인스턴스와 동그라미 인스턴스를 생성한 후에
rectangle = Rectangle(3, 7)
circle = Cicle(4)
캔버스에 도형을 추가한다.
canvas = Canvas()
canvas.add_shape(rectangle)
canvas.add_shape(circle)
그리고 canvas.total_area_of_shapes()
를 실행하면, 클래스 Canvas
내부의 메소드 total_area_of_shapes
가 실행되며 sum([shape.area() for shape in self.shapes])
가 실행된다. 이때, shape
라는 변수에 Circle
클래스에서 생성된 인스턴스와 Rectangle
클래스에서 생성된 인스턴스가 번갈아가면서 들어가게 되는데 이렇게 다양한 클래스의 인스턴스가 하나의 변수에 들어갈 구 있는 성질을 다형성이라고 한다.
한편, 이렇게 된다면 area
메소드가 없는 원통 도형일 경우에는 에러가 발생하게 된다.
이 경우, 상속을 활용하면 된다.
즉, Rectangle이나 Circle과 같은 평면 도형을 아우르는 Shape 클래스를 설정해놔서 상속받도록 한다.
여기서 Shape는 Canvas에 추가할 수 없는 추상적인 계층이며, 캔버스에 추가되는 도형은 직사각형이나 원과 같은 구체적인 도형일 것이다.
이를 코딩으로 구현하면 다음과 같다.
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
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
class Cylinder:
"""원통 클래스"""
def __init__(self, radius, height):
self.radius = radius
self.height = height
def __str__(self):
"""원통의 정보를 문자열로 리턴하는 메소드"""
return "밑면 반지름 {}, 높이 {}인 원기둥".format(self.radius, self.height)
class Canvas:
def __init__(self):
self.shapes = []
def add_shape(self, shape):
"""캔버스에 도형을 추가한다"""
if instance(shpe, Shape):
self.shapes.append(shape)
else:
print("넓이, 둘레를 구하는 메소드가 없는 도형은 추가할 수 없습니다.")
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])
def __str__(self):
"""캔버스에 있는 각 도형들의 정보를 출력한다"""
res_str = "그림판 안에 있는 도형들:\n\n"
for shape in self.shapes:
res_str += str(shape) + "\n"
return res_str
cylinder = Cylinder(7, 4)
rectangle = Rectangle(3, 7)
circle = Cicle(4)
canvas = Canvas()
canvas.add_shape(cylinder)
canvas.add_shape(rectangle)
canvas.add_shape(circle)
print(canvas.total_area_of_shapes())
print(canvas.total_perimeter_of_shapes())
하지만 상속을 받는다고 해도 area
메소드를 오버라이딩하지 않고 클래스를 작성할 경우 해당 에러가 발생하게 된다.
이때 메소드를 작성(오버라이딩)하도록 강제하는 것이 추상클래스이다.
추상클래스
: 구체적으로 정의하지 않고 추상적으로 골조를 정의해둔 것 + 파이썬에서 특정 문법 추가
모듈 임포트
from abc import ABC, abstractmethod
Abstract
Base
Class의 준말이 ABC이다. 추상화 기초 클래스인 것.
이 클래스를 상속받으면 추상클래스로 만들 수 있는 것이다.
추상메소드: 자식 클래스가 반드시 오버라이딩해야하는 메소드
위의 두 방법을 사용하여 Shape
클래스를 추상 클래스로 재정의하면 다음과 같다.
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, perimete
한편, ABC를 상속하지 않고 추상메소드 데코레이션만 사용하여 작성할 경우 추상클래스로 인스턴스 생성도 되고, 데코레이터도 잘 듣지 않는다.
즉, 제대로 추상화되지 않는다.
따라서 두가지 요소 (1) 추상 클래스 생성 시 ABC
상속, (2) 추상메소드를 작성할 때는 @abstractmethod 데코레이터 사용을 둘다 지켜주도록 하자.
위의 예시에서 추상화를 끝마친 Shape
클래스를 상속받는 또다른 구체적인 도형 클래스를 생성해보자.
class EquilateralTriangle(Shape):
"""정삼각형 클래스"""
def __init__(self, side):
self.side = side
와 같은 또 다른 클래스를 생성할 때, @abstractmethod
가 표시된 추상 메소드를 오버라이딩하지 않았으므로 에러가 발생하게 된다.
그런데 그 에러를 가만히 살펴보면, 추상클래스 Shape
의 인스턴스를 생성할 때와 비슷한 에러가 뜬다.
TypeError: Can't instantiate abstract class EquilateralTriangle with abstract methods area, perimeter
이는, 정삼각형 클래스가 현재 추상화 기초 클래스(ABC
)를 상속받은 Shape
를 상속받은 자식클래스이기 때문에 추상화 클래스로 존재하기 때문이다! 이때 추상 메소드 area
와 perimeter
를 오버라이딩해주면 정삼각형 클래스는 더이상 추상메소드를 보유하지 않게 되고, (추상클래스를 상속받고 있기 때문에 여전히 @abstractmethod
데코레이터가 작동할 수 있는 성질은 지니지만) 일반클래스처럼 행동이 가능해지고, 결과적으로 인스턴스도 정상적으로 생성할 수 있게 된다.
Tip 1 추상화 클래스를 작성할 시에, type hinting을 해주어 변수, 메소드의 파라미터나 리턴값에 대한 힌트를 표시해주자!
그럼 오버라이딩 시에 더 수월할 것😚
class Shape(ABC):
"""도형 클래스"""
@abstractmethod
def area(self) -> float:
"""도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
@abstractmethod
def perimeter(self) -> float :
"""도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
Tip 2 첫번째 시간으로 돌아가 '추상화'의 정의에 대해 떠올려보면, 추상화란 변수, 함수, 클래스를 사용해 사용자가 꼭 알아야만 하는 부분만 겉으로 드러내는 것이다. 추상 클래스 역시 이 '추상화'의 기본 정신, 스피릿을 이어받았다!
추상 클래스는 서로 관련있는 클래스들의 공통 부분을 묶어서 추상화한다.
Tip 3 추상메소드에도 내용을 채우는 경우가 있다! 모든 자식 클래스에서 공통적으로 사용할 부분을 추상 메소드의 내용으로 써주고 자식클래스에서 이를 super
함수로 접근한다...
from abc import ABC, abstractmethod
class Shape(ABC):
"""도형 클래스"""
@abstractmethod
def area(self) -> float:
"""도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
print("도형의 넓이 계산 중!") # ---------------- 추가된 코드
@abstractmethod
def perimeter(self) -> float:
"""도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
class Rectangle(Shape):
"""직사각형 클래스"""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
"""직사각형의 넓이를 리턴한다"""
super().area() # ---------------- 부모의 메소드를 가져다 씀
return self.width * self.height
def perimeter(self):
"""직사각형의 둘레를 리턴한다"""
return 2*self.width + 2*self.height
Tip 4 추상클래스로 '메소드'뿐만 아니라, '변수'도 갖도록 강제할 수 있다.
class Shape(ABC):
"""도형 클래스"""
@abstractmethod
def area(self) -> float:
"""도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
print("도형 넓이 계산 중!")
@abstractmethod
def perimeter(self) -> float:
"""도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
def __str__(self):
return "추상 클래스라고 해서 모든 메소드가 추상 메소드일 필요는 없습니다!"
@property
@abstractmethod
def x(self):
"""도형의 x 좌표 getter 메소드"""
pass
@property
@abstractmethod
def y(self):
"""도형의 y 좌표 getter 메소드"""
pass
위와 같이 @property
와 @abstractmethod
를 동시에 쓰면, 이 메소드는 getter메소드이자 추상 메소드가 된다.
한편, 상속받는 자식 클래스에서는 아래와 같이 getter/ setter 메소드를 작성하여 변수를 세팅하는 것과 유사하게 할 수 있다.
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
equilateral_triangle.y = 5
print(equilateral_triangle.y) # 출력: 5
Tip 5 추상클래스의 다중상속
함수/ 메소드의 다형성
- 옵셔널 파라미터(optional parameter) 사용
: 기본값(default)을 미리 지정해놓은 파라미터
: 옵셔널 파라미터는 가장 뒤에 몰아서 배열해야 한다. - 함수 호출 시에 파라미터 이름을 명시
- 개수가 확정되지 않은 파라미터
: 개수를 확정하지 않은 파라미터 앞에*
를 붙여주면 튜플에 담긴다.
LBYL 코딩 스타일
: Look Before You Leap
: 뛰기 전에 살펴보라 (돌다리도 두드려보고 건너라)
EAFP 코딩 스타일
: Easier to Ask for Forgiveness than Permission
: 허락보다 용서가 쉽다!
: 일단 해보고 문제가 생기면 그때 해결하자~😆
예를 들어, Canvas
를 LBYL 스타일로 작성하면 아래와 같다.
class Canvas:
def __init__(self):
self.shapes = []
def add_shape(self, shape):
"""캔버스에 도형을 추가한다"""
if instance(shpe, Shape):
self.shapes.append(shape)
else:
print("넓이, 둘레를 구하는 메소드가 없는 도형은 추가할 수 없습니다.")
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])
def __str__(self):
"""캔버스에 있는 각 도형들의 정보를 출력한다"""
res_str = "그림판 안에 있는 도형들:\n\n"
for shape in self.shapes:
res_str += str(shape) + "\n"
return res_str
이를 EAFP 스타일로 작성해보면 아래와 같다.
class Canvas:
def __init__(self):
self.shapes = []
def add_shape(self, shape: Shape):
"""캔버스에 도형 인스턴스 shape를 추가한다. 단, shape는 추상 클래스 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))
return total_area
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))
return total_perimeter
def __str__(self):
"""캔버스에 있는 각 도형들의 정보를 출력한다"""
res_str = "그림판 안에 있는 도형들:\n\n"
for shape in self.shapes:
res_str += str(shape) + "\n"
return res_str
파이썬 문화에 따르면 EAFP 코딩 스타일이 조금 더 적합하다고는 하지만, 내 코딩 스타일은 LBYL 코딩 스타일에 조금 더 가까운듯 하다.
무엇보다 애초에 추가할 때 isinstance
한번만 해주면 될걸 굳이 try/ except
구문을 번거롭게 사용해 가면서 해야겠냐고...
'IT Anthology > encyclopedia' 카테고리의 다른 글
[객체 지향 프로그래밍] SOLID 원칙 (0) | 2022.03.18 |
---|---|
[객체 지향 프로그래밍] 상속 (0) | 2022.03.14 |
[객체 지향 프로그래밍] 캡슐화 (0) | 2022.03.14 |
[객체 지향 프로그래밍] 추상화 (0) | 2022.03.14 |
[OOP] 객체 지향 프로그래밍에 대한 기본 개념 (0) | 2022.03.01 |