추상화
사물에서 사용에 꼭 필요한 부분만 드러내는 것
변수의 값이나, 메소드 내부의 모든 내용을 샅샅이 알지 못해도 그것들을 사용할 수 있다.
추상화 잘하기: 이름 잘 짓기
어디에 쓰는 클래스이고, 어떻게 사용할지 직관적으로 알 수 있다. 클래스,변수 이름과 메소드 이름이 잘 지어져야한다.
추상화 더 잘하기: 문서화하기
docstring: documentation string
"""
이것은 docstring입니다. 이런 느낌
이렇게 여러 줄을 써도 하나의 docstring입니다.
"""
설명해주고 싶은 클래스나 메소드 아래에 바로 써준다.
help(BankAccount) #docstring을 바로 보여준다.
Python
복사
문서화의 형식에 관해 꼭 지켜야할 규칙은 없습니다. 하지만 흔히 사용하는 포맷은 있습니다.
유저를 위한 추천 영상을 찾는 find_suggestion_videos 메소드의 docstring을 작성한다고 해봅시다. 널리 쓰이는 포맷 3가지로 각각 문서화를 해볼게요.
def find_suggestion_videos(self, number_of_suggestions=5):
Plain Text
복사
Google docstring:
"""유저에게 추천할 영상을 찾아준다
Parameters:
number_of_suggestions (int): 추천하고 싶은 영상 수
(기본값은 5)
Returns:
list: 추천할 영상 주소가 담긴 리스트
"""
Plain Text
복사
reStructuredText (파이썬 공식 문서화 기준):
"""유저에게 추천할 영상을 찾아준다
:param number_of_suggestions: 추천하고 싶은 영상 수
(기본값은 5)
:type number_of_suggestions: int
:returns: 추천할 영상 주소가 담긴 리스트
:rtype: list
"""
Plain Text
복사
NumPy/SciPy (통계, 과학 분야에서 쓰이는 Python 라이브러리):
"""유저에게 추천할 영상을 찾아준다
Parameters
----------
number_of_suggestions: int
추천하고 싶은 영상 수 (기본값은 5)
Returns
-------
list
추천할 영상 주소가 담긴 리스트
"""
Plain Text
복사
한 가지 메소드의 정보를 3가지 포맷으로 문서화한 것을 보았습니다. 문서화에서 가장 중요한 것은 프로그램을 함께 만드는 팀원들과 이러한 문서화 포맷에 관해 미리 약속을 하고 이를 잘 지키는 것입니다. 혼자서 만드는 프로그램이라도 자신만의 포맷을 일관성있게 사용한다면 나중에 프로그램을 수정할 때 도움이 되겠죠?
파이썬의 type hinting
동적 타입 언어
정수형인지 문자열인지 표시한다.
정적 타입 언어 처럼 타입을 표시할 수 있는 기능이다.
class BankAccount:
interest: float = 0.02
def __init__(self,name:str,balance: float) -> None:
self.name = name
self.balance = balance
def deposit(self,amount: float) ->None:
self.balance += amount
def withdraw(self,amount:float) -> str:
if(self.balance < amount):
print("Insufficient balance!")
else:
self.balance -= amount
Python
복사
type hinting은 실행에 영향을 주진 않지만, 지키기를 권장한다.
캡슐화
캡슐화
•
객체의 일부 구현 내용에 대한 외부로부터의 직접적인 엑세스를 차단하는 것
•
객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것
class Citizen:
dringking_age = 19
def __init__(self,name,age,resident_id):
self.name = name
self.__age = age
self.__resident_id = resident_id
def __authenticate(self,id_field):
return self.__resident_id == id_field
def can_drink(self):
return self.__age >= Citizen.dringking_age
def __str__(self):
return self.name + "씨는 " + str(self.__age) + "살입니다!"
JavaScript
복사
이런 __age 이런 식으로 함수 외부에서의 호출을 막고
메소드도 마찬가지로 할 수 있다.
변수에 접근할 수 있는 메소드를 따로 설계한다.
—> 변수에 접근하는 통로를 메소드로 제한하는 것을 '하나로 묶는다'라고 얘기한다.
사실 아닙니다~!! 짜잔
파이썬에는 완벽한 캡슐화가 없다
__age라고 하면 네임 맹글링이 된다
즉 Citizen 클래스에 __age라고 하면 _Citizen__age이 된다.
이름이 바뀌면서 접근이 안되는 것일뿐
캡슐화에 대한 문화가 있다, 함부로 접근하지 마라는 표시를 하는것
언더바 하나를 쓰는 거다. 이런 거는 그냥 다른 개발자들에게 말하는 경고표시다
class Citizen:
dringking_age = 19
def __init__(self,name,age,resident_id):
self.name = name
self.set_age(age)
self._resident_id = resident_id
def authenticate(self,id_field):
return self._resident_id == id_field
def can_drink(self):
return self._age >= Citizen.dringking_age
def __str__(self):
return self.name + "씨는 " + str(self._age) + "살입니다!"
def get_age(self):
return self._age
def set_age(self,value):
if value < 0:
self._age = 0
else:
self._age = value
young = Citizen("younghoon kang",18, "12345678")
print(young.get_age())
Python
복사
데코레이터를 사용한 캡슐화
왜 언어 자체에서 캡슐화를 지원하지 않을까? → 그냥 파이썬 문화 때문이다.
나중에 변수를 숨기고 싶으면 어떡할까
property라는 데코레이션을 쓰는거다.
property 데코레이터 → 변수의 값을 읽거나 설정하는 구문이 아예 다른 의미로 실행
class Citizen:
dringking_age = 19
def __init__(self,name,age,resident_id):
self.name = name
self.set_age(age)
self._resident_id = resident_id
def authenticate(self,id_field):
return self._resident_id == id_field
def can_drink(self):
return self._age >= Citizen.dringking_age
def __str__(self):
return self.name + "씨는 " + str(self._age) + "살입니다!"
@property
def age(self):
print("나이를 리턴합니다.")
return self._age
@age.setter
def age(self,value):
if value < 0:
self._age = 0
else:
self._age = value
young = Citizen("younghoon kang",15, "12345678")
print(young.age)##getter 메소드
young.age = 30 ##세터 메소드
print(young.age)
Python
복사
프로퍼티 데코레이터 함수를 적용하면, getter와 setter가 적용된 부분을 내가 다 바꿀 필요가 없다.
그냥 age함수에 대해서 프로퍼티 데코레이터만 붙이면 된다.
객체를 사용할 땐 최대한 메소드로
코드를 유지보수 하기 힘들다 → 변수를 직접 가져다 쓰면 힘들다.
이미 그 변수를 이용해서 기능을 만드는 메소드가 있는데, 굳이 변수를 직접 접근하면 유지보수하기 힘들어진다.
상속
상속이란?
두 클래스 사이에 부모 - 자식 관계를 설정하는 것
B is A, but A is not B
B: 자식, A: 부모
mro 메소드
이전 영상에서 help 함수의 실행 결과 중 Method resolution order:라는 부분을 보았습니다. 이 부분에 있던 결과는 해당 인스턴스의 클래스가 어떤 부모 클래스를 가지는지 보여주는데요. 이 결과를 다른 방법으로도 볼 수 있습니다. 바로 모든 클래스가 갖고 있는 mro라는 메소드를 호출하면 됩니다. 아래 코드에서 Cashier 클래스로 mro 메소드를 호출해보면
class Employee:
"""직원 클래스"""
raise_percentage = 1.03
company_name = "코드잇 버거"
def __init__(self, name, wage):
"""인스턴스 변수 설정"""
self.name = name
self.wage = wage
def raise_pay(self):
"""직원 시급을 인상하는 메소드"""
self.wage *= Employee.raise_percentage
def __str__(self):
"""직원 정보를 문자열로 리턴하는 메소드"""
return Employee.company_name + " 직원: " + self.name
class Cashier(Employee):
pass
class Manager(Employee):
pass
print(Cashier.mro())
Python
복사
이렇게 출력됩니다.
실행 결과
[<class '__main__.Cashier'>, <class '__main__.Employee'>, <class 'object'>]
Python
복사
이렇게 하면 Cashier 클래스가 상속하는 부모 클래스를 볼 수 있습니다. 이 경우에 object 클래스는 Cashier 클래스의 입장에서 부모 클래스의 부모 클래스입니다.
우리가 자주 쓰는 파이썬의 기본 클래스 중 하나인 list 클래스의 mro 메소드의 실행 결과를 살펴볼까요?
print(list.mro())
Plain Text
복사
실행 결과
[<class 'list'>, <class 'object'>]
Plain Text
복사
결과를 보니 상속받는 클래스가 최상위 클래스 object 하나밖에 없네요.
이번에는 파이썬에서 들여쓰기를 잘못했을 때 나오는 에러를 나타내는 IndentationError 클래스를 보겠습니다.
print(IndentationError.mro())
Plain Text
복사
실행 결과
[<class 'IndentationError'>, <class 'SyntaxError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>]
Plain Text
복사
IndentationError 클래스는 부모, 부모의 부모, 부모의 부모의 부모, 부모의 부모의 부모의 부모도 있군요. 최상위 클래스인 object 클래스부터, 파이썬 문법 위반 시 발생하는 SyntaxError 클래스까지 IndentationError 클래스의 집안 내력(?)을 순서대로 한 번에 파악할 수 있습니다.
isinstance 함수
isinstance 함수는 어떤 인스턴스가 주어진 클래스의 인스턴스인지를 알려줍니다. isinstance 함수의
1.
첫 번째 파라미터에는 검사할 인스턴스의 이름
2.
두 번째 파라미터에는 기준 클래스의 이름
을 넣고 실행하면 되는데요. 이렇게 하면 그 인스턴스가 해당 클래스의 인스턴스인지를 불린 값(True 또는 False)으로 리턴합니다.
아래 코드를 봅시다.
# 인스턴스를 생성한다
young = Cashier("강영훈", 8900)
print(isinstance(young, Cashier))# 출력: True
print(isinstance(young, DeliveryMan))# 출력: False
print(isinstance(young, Employee))# 출력: True
Plain Text
복사
young 인스턴스는 Cashier 클래스의 인스턴스니까 True를 리턴했네요. 하지만 DeliveryMan 클래스의 인스턴스는 아니기 때문에 False를 리턴했습니다.
여기서 중요한 것은 마지막 줄에서 isinstance(young, Employee) 가 True 를 리턴한다는 사실입니다.
Cashier 클래스는 Employee 클래스를 상속받는 자식 클래스입니다.
이 점이 아주아주 중요한데요.
즉, 상속 관계에 있는 두 클래스가 있을 때, 자식 클래스로 만든 인스턴스는 부모 클래스의 인스턴스이기도 하다는 점을 뜻합니다.
이 점은 나중에 ‘다형성’이라는 것을 설명할 때 핵심이 되는 원리입니다. 잊지 말고 꼭 기억해주세요!
issubclass 함수
issubclass 함수는 한 클래스가 다른 클래스의 자식 클래스인지를 알려주는 함수입니다.
1.
첫 번째 파라미터로 검사할 클래스의 이름
2.
두 번째 파라미터에는 기준이 되는 부모 클래스의 이름
를 넣고 실행하면 됩니다.
아래 코드를 봅시다.
print(issubclass(Cashier, Employee))# 출력: True
print(issubclass(Cashier, object))# 출력: True
print(issubclass(Manager, Employee))# 출력: True
print(issubclass(Employee, list))# 출력: False
Plain Text
복사
상속 관계가 있는 경우에는 모두 True를 리턴했네요. 마지막 줄에서 Employee 클래스는 list 클래스와 아무런 관련이 없으니까 False를 리턴했습니다
오버라이딩
물려받은 내용을 자식 클래스가 자신한테 맞게 바꿔쓰는 것 → 오버라이딩
class Employee:
company_name = " 코드잇 버거"
raise_percentage = 1.03
def __init__(self,name,wage):
self.name = name
self.wage = wage
def raise_pay(self):
self.wage *= self.raise_percentage
def __str__(self):
return Employee.company_name + " 직원: " + self.name
class Cashier(Employee):
raise_percentage = 1.05
##변수 오버라이딩
def __init__(self,name,wage,number_sold):
super().__init__(name,wage)
#super함수로 메소드에 파라미터를 넘겨줄 때는 , self가 필요가 없다.
self.number_sold = number_sold
def __str__(self):
return Cashier.company_name + " 계싼대 직원: " + self.name
yong = Cashier("강영훈",8900)
Python
복사
mro
mro를 하면 상속받는 클래스의 리스트가 나온다.
이 때 오버라이딩 하게 되면 해당 클래스 순서대로 메소드를 찾아서 반환한다.
mro → method resolution order
메소드 검색방향을 알려준다. 자식 → 부모 방향으로 나열되있다
다중 상속이 가능하다
class Engineer:
def __init__(self,favorite_language):
self.favorite_lanaguage = favorite_language
def program(self):
print("{}으로 프로그래밍합니다".format(self.favorite_lanaguage))
class TennisPlayer:
def __init__(self,tennis_level):
self.tennis_level = tennis_level
def play_tennis(self):
print("{} 반에서 테니스를 칩니다".format(self.tennis_level))
class EngineerTennisPlayer(Enginner,TennisPlayer):
def __init__(self,favorite_language,tennis_level):
Engineer.__init__(self,favorite_language)
TennisPlayer.__init__(self,tennis_level)
Python
복사
super를 쓰지 못한다 → 단점
다중상속의 위험성
mro는 클래스의 상속 관계에 따라 바뀔 수 가 있다.
이에 따라서 상속받은 메소드를 사용할 때, 어떤 메소드가 선택되어질지 힘들다.
부모 클래스끼리 같은 이름의 메소드를 갖지 않도록 하기
같은 이름의 메소드는 자식 클래스에서 오버라이딩 한다.
다형성
어떤 변수가 여러 클래스의 인스턴스가 될 수 있는 것
상속을 활용한 다형성
from math import pi
class Shape:
def are(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
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 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
Python
복사
여서 shape.area()라는 메소드가 모든 shape가 다 가지고 있기 때문에 가능하다.
이런 일반 상속의 문제점?
자식 클래스가 오버라이딩할 함수를 하지 않았는데도, 특정 클래스를 상속함으로써 그 클래스의 범위 안에 들어갈 수 있다.
from math import pi
class Shape:
def are(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
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,side):
self.side = side
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
Python
복사
상속 받긴 하지만, 메소드를 오버라이딩하지는 않는다 → 강제로 메소드를 오버라이딩하게끔
추상클래스를 만들면 된다.
추상클래스로는 인스턴스를 만들 수 없다.
인스턴스를 생성하려고 만드는게 아니라, 여러 공통점을 만들어놓고 상속받는 목적으로 만들어놓는거다.
from math import pi,sqrt
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod #추상 메소드
def are(self)->float:
#도형의 넓이에 따라 다르게 오버라이딩 해야함
pass
@abstractmethod
def perimeter(self)->float:
#둘레에 따라 다르게 오버라이딩 해야함
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
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,side):
self.side = side
def are(self):
return sqrt(3) * self.side * self.side / 4
def perimeter(self):
return 3* self.side
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
Python
복사
추상 클래스 더 알아보기
1. 추상 클래스와 추상화!
우리는 지금 “객체 지향 프로그래밍의 4가지 기둥” 중 마지막에 해당하는 “다형성”을 배우고 있습니다. 혹시 첫 번째로 배웠던 “추상화” 기억나시나요? 추상화는 변수, 함수, 클래스를 사용해 사용자가 꼭 알아야만 하는 부분만 겉으로 드러내는 것이라고 배웠습니다. 이번에 배운 추상 클래스도 이러한 추상화의 한 예시입니다.
추상 클래스는 서로 관련있는 클래스들의 공통 부분을 묶어서 추상화합니다. 무슨 말인지 예시를 통해 차근차근 이해해보죠. 다음 코드는 이전 영상의 그림판 프로그램 클래스입니다.
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
Python
복사
1.
Paint 클래스를 사용하는 개발자는 add_shape 메소드에서 파라미터 shape으로 들어오는 인스턴스의 타입이 Shape 클래스일 때만 그 인스턴스를 추가합니다. 이는 해당 인스턴스가 구체적으로 무슨 도형의 인스턴스인지는 관심이 없고 Shape 클래스의 인스턴스에만 해당하면 된다는 뜻입니다.
2.
여기서 Shape 클래스는 추상 클래스입니다. 따라서 Shape 클래스의 인스턴스라는 것은 그 인스턴스의 클래스가 Shape 클래스를 상속받은 자식 클래스로, 추상 메소드 area와 perimeter를 오버라이딩한 클래스여야한다는 뜻이죠.
3.
정리하자면 도형을 나타내는 클래스라면 가질 수 밖에 없는 공통점을 Shape 클래스로 추상화한 것입니다.
이렇게 하면 Paint 클래스의 코드를 작성하는 개발자는 추상 클래스로 추상화된 수준(Shape 클래스)까지만 고려하고 개발을 진행할 수 있습니다. 그러니까 개발자는 추가된 각 도형 인스턴스가 구체적으로 무슨 클래스의 인스턴스인지 확인할 필요없이, 일단 area, perimeter 메소드를 가지는 인스턴스라고 생각하고 개발할 수 있는 것이죠.
이 상황에서 좀더 추가하자면 add_shape 메소드에 Shape 타입을 가지는 인스턴스가 shape 파라미터로 들어와야 한다는 것을 알려주기 위해 다음과 같이 파이썬의 type hinting 기능을 사용할 수 있습니다.
def add_shape(self, shape: Shape):
Plain Text
복사
이런 type hinting 자체만으로 Shape 클래스의 인스턴스만 들어오도록 강제할 수는 없지만, 이런 정보를 둬야 개발자가 Paint 클래스를 제대로 사용할 수 있겠죠?
2. 추상 클래스에도 일반 메소드를 추가할 수 있어요!
추상 클래스에 꼭 추상 메소드만 있어야하는 것은 아닙니다. @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()
Plain Text
복사
Shape 클래스 중 larger_than 메소드가 일반 메소드입니다. 이 메소드는 파라미터로 전달된 다른 도형 인스턴스의 넓이와 자신의 넓이를 비교합니다.
Shape 클래스를 상속받는 원(Circle) 클래스를 만들고 원 인스턴스로 일반 메소드 larger_than을 호출해보면
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
circle = Circle(6)
rectangle = Rectangle(3, 4)
print(circle.larger_than(rectangle))# 출력: True
Plain Text
복사
제대로 작동합니다. 즉, 추상 클래스에는 꼭 추상 메소드뿐만 아니라 일반 메소드도 정의할 수 있고 이것도 똑같이 자식 클래스가 물려받습니다. 하지만 차이점이 있다면
1.
반드시 오버라이딩해야하는 추상 메소드와 달리
2.
일반 메소드는 물려받은 그대로 사용할지, 오버라이딩할지를 자식 클래스에서 결정하는 것이구요.
3. 추상 메소드에도 내용을 채울 수 있습니다!
from abc import ABC, abstractmethod
class Shape(ABC):
"""도형 클래스"""
@abstractmethod
def area(self) -> float:
"""도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
@abstractmethod
def perimeter(self) -> float:
"""도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
Plain Text
복사
지금까지는 추상 메소드의 내용으로 그냥 pass만 써줬습니다. 하지만 사실 추상 메소드 안에는 다른 내용을 써도 됩니다. 아래 코드처럼요!
from abc import ABC, abstractmethod
class Shape(ABC):
"""도형 클래스"""
@abstractmethod
def area(self) -> float:
"""도형의 넓이를 리턴한다: 자식 클래스가 오버라이딩할 것"""
print("도형의 넓이 계산 중!")# ---------------- 추가된 코드 @abstractmethod
def perimeter(self) -> float:
"""도형의 둘레를 리턴한다: 자식 클래스가 오버라이딩할 것"""
pass
Plain Text
복사
그런데 좀 이상하죠? 어차피 추상 클래스를 상속받는 자식 클래스에서 이 추상 메소드들은 반드시 오버라이딩해야 합니다. 그래서 이렇게 어차피 무시될 추상 메소드의 내용이 왜 필요한지 모르겠군요.
하지만 사실 이 내용은 경우에 따라 유용할 때가 있습니다. 보통 추상 메소드에 내용을 쓸 때는 모든 자식 클래스에 해당하는 공통 내용을 써줍니다. 그리고 자식 클래스에서 추상 메소드를 오버라이딩하더라도 이렇게 미리 채워진 내용을 가져와서 재활용할 수 있습니다. 이는 super 함수를 사용하면 가능합니다.
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
rectangle = Rectangle(3, 4)
print(rectangle.area())# 출력: 도형의 넓이 계산 중! 12
Plain Text
복사
예전에 부모 클래스의 __init__ 메소드를 사용할 때 자식 클래스에서 super 함수로 부모 클래스의 내용에 접근할 수 있다고 설명한 적이 있는데 혹시 기억하시나요? super 함수를 사용하면 부모 클래스에 접근할 수 있습니다. 이 코드 중 area 메소드를 보세요. 부모 클래스인 Shape 클래스의 area 메소드를 실행하는 부분이 있습니다.
즉, 물려받은 추상 메소드를 오버라이딩하는데
1.
super 함수를 통해 추상 메소드의 기존 내용(print("도형 넓이 계산 중!"))을 포함함과 동시에
2.
이와 별도로 자신만의 내용을 또 추가한거죠.(return self.width * self.height)
이렇게 모든 자식 클래스에서 공통적으로 사용할 부분을 추상 메소드의 내용으로 써주고 자식 클래스에서 이를 super 함수로 접근하는 방법은 꽤 자주 쓰는 방법입니다. 이번 기회에 꼭 기억하세요!
4. 자식 클래스가 특정 변수를 갖도록 유도할 수 있어요!
추상 클래스를 사용하면 자식 클래스가 추상 클래스의 추상 메소드를 오버라이딩하도록 즉, 해당 메소드를 갖도록 강제할 수 있습니다. 하지만 이밖에도 추상 클래스로 자식 클래스가 특정 변수를 갖도록 유도할 수 있는 방법이 있습니다. 예시를 통해 알아볼까요? 이 부분이 이 노트의 4가지 내용 중 가장 어려운데요, 하나씩 살펴봅시다.
그림판에서 사용할 모든 도형 클래스는 좌표를 나타내는 인스턴스 변수 x와 y를 반드시 가져야한다고 가정합시다. 어떻게 하면 추상 클래스를 사용해 각 자식 클래스가 이 변수를 갖도록 유도할 수 있을까요?
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
Python
복사
위 코드를 보세요. 이 Shape 클래스는 지금 x 메소드와 y 메소드를 getter 메소드이자 추상 메소드로 갖고 있습니다. @property는 파이썬스럽게 getter/setter 메소드를 정의하는 방법에서 배웠던 데코레이터입니다. 기억나시죠?
이렇게 @property 와 @abstractmethod 데코레이터를 메소드 이름 위에 연달아 적어주면 이 메소드
는 getter 메소드이자 추상 메소드가 됩니다. 즉, 어떤 변수에 대한 getter 메소드를 뜻하지만 아직 내용이 비어있어 어떤 변수를 리턴하는지는 결정되지 않은 것이죠. 이때 Shape 클래스를 상속받는 자식 클래스에서 어떤 변수를 리턴하는지 즉, 이 getter 메소드가 어떤 변수에 대한 것인지를 나타내도록 오버라이딩해야하는 것입니다.
일단 Shape 클래스를 상속받는 정삼각형 클래스인 EquilateralTriangle 클래스를 정의했습니다. 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(4)# 에러 발생: TypeError: Can't instantiate abstract class EquilateralTriangle with abstract methods x, y
Python
복사
추상 메소드 x, y를 오버라이딩하지 않아서 생긴 에러입니다.
그렇다면 각 getter 메소드는 어떻게 오버라이딩하면 될까요?
보통
1.
인스턴스 변수의 이름은 예를 들어 _apple 처럼 캡슐화를 적용한 것으로 나타내고
2.
getter 메소드의 이름은 apple 처럼 캡슐화된 변수 이름 앞에서 밑줄을 뺀 이름
으로 한다고 배웠습니다. 이 경우에 적용한다면 x는 인스턴스 변수 _x의 getter 메소드로, y는 인스턴스 변수 _y의 getter 메소드로 해주면 좋을 것 같네요.
@property
def x(self):
"""_x getter 메소드"""
return self._x
@x.setter
def x(self, value):
"""_x setter 메소드"""
self._x = value
Plain Text
복사
혹시 @property 데코레이터의 기능이 잘 생각나지 않는 분을 위해 설명하자면
이 코드의 의미는 이 클래스의 인스턴스에 대해 self.x , 인스턴스 이름.x 와 같은 부분을 실행할 때, getter 메소드 x를 실행한다는 의미입니다. 즉, @property가 붙으면 이런 구문들이 인스턴스 변수 x의 값을 직접 읽는다는 원래의 뜻이 아니라 getter 메소드 x를 실행한다는 의미로 바뀌는 거죠.
그 아래의 @x.setter 가 붙은 메소드는 이 클래스의 인스턴스에 대해 self.x = 3 , 인스턴스 이름.x = 3 과 같은 부분을 실행할 때 setter 메소드 x를 실행한다는 의미입니다. 즉, @x.setter가 붙으면 이런 구문들이 인스턴스 변수 x에 어떤 값을 설정한다는 원래의 뜻이 아니라 setter 메소드 x를 실행한다는 의미로 바뀌는 것이구요.
그럼 이때까지 설명한 조건에 부합하는 EquilateralTriangle 클래스를 완성한 결과를 봅시다.
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
Plain Text
복사
이 코드는 잘 실행됩니다. 물론 Shape 클래스에서 자식 클래스에 getter 메소드를 오버라이딩하도록 강제한다고 해도 자식 클래스에서 이 메소드를 변수의 내용을 가져오는 getter 메소드로서의 내용이 아닌 아예 엉뚱한 내용으로 오버라이딩할 수도 있습니다. 하지만 파이썬의 문화를 잘 따르는 개발자라면 getter/setter 메소드의 내용이 되도록 오버라이딩할 것입니다. 이처럼 부모 클래스에서 추상 메소드인 getter 메소드를 만들어서 자식 클래스가 그 getter 메소드의 대상이 되는 인스턴스 변수를 갖도록 유도할 수 있는 것입니다!
그리고
1. 추상 클래스 다중 상속은 일반적으로 많이 사용한다.
2. 다중 상속받는 부모 추상 클래스들이 추상 메소드로만 이뤄져 있으면 아무 문제 없이 다중 상속받을 수 있다.
3. 다중 상속받는 부모 추상 클래스들 간에 이름이 겹치는 일반 메소드가 있으면 일반 클래스를 다중 상속받을 때와 동일한 문제가 생길 수 있다.
함수/메소드 다형성
여러 가지 형태로 함수를 호출하는 것
#optional parameter
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())
# *를 이용하여 나머지 파라미터를 받기
def print_message_and_add_numberS(message, *numbers):
print(message)
return sum(numbers)
Python
복사
어떤 작업을 수행하기 전에 확인하는 것 → LBYL
Easier to Ask for Forgiveness than permission → EAFP : 일단 하고 생각하자