다형성(polymorphism, 여러 모습을 가짐)은 같은 이름의 메서드를 불렀는데 객체마다 다르게 동작하는 성질이다. speak()라는 한 번의 호출이 고양이에게는 야옹을, 개에게는 멍멍을 내게 하는 것이 다형성이다. 이 성질 덕분에 부르는 쪽은 객체가 어떤 종류인지 일일이 따지는 if 분기를 두지 않고, 여러 종류를 똑같은 방식으로 다룰 수 있다.
파이썬은 여기서 한 걸음 더 나간다. 상속 관계가 전혀 없어도, 필요한 메서드만 갖고 있으면 같이 다룬다. 이것을 덕 타이핑(duck typing)이라 부른다. 오리처럼 걷고 오리처럼 운다면 그것이 무슨 클래스든 오리로 취급한다는 뜻이다. 이번 편은 다형성과 덕 타이핑을 다섯 예제로 확인한다.
07.1 같은 호출, 다른 동작
![[OOP 07] 다형성과 덕 타이핑](https://img.thenullpage.com/posts/6626/6626_1_017a0b.webp)
고양이, 개, 소 세 클래스가 각각 speak()를 갖는다. 이들을 한 리스트에 담고 똑같이 speak()를 부르면 각자 자기 소리를 낸다.
class Cat3:
def speak(self):
return "야옹"
class Dog3:
def speak(self):
return "멍멍"
class Cow:
def speak(self):
return "음메"
for animal in [Cat3(), Dog3(), Cow()]:
print(animal.speak())
야옹
멍멍
음메
반복문 안의 animal.speak()는 글자 하나 바뀌지 않는 똑같은 코드다. 그런데 animal에 담긴 객체가 무엇이냐에 따라 결과가 달라진다. 반복문은 지금 다루는 동물이 고양이인지 소인지 전혀 신경 쓰지 않는다. 각 객체가 자기 소리를 아는 것으로 충분하다.
07.2 if 분기 나열을 없애다
다형성이 없다면 같은 일을 어떻게 할까. 종류를 문자열로 받아 if로 갈래를 나누는 방식이 흔하다. 문제는 동물이 하나 늘 때마다 이 함수를 뜯어고쳐야 한다는 점이다.
def make_sound_bad(kind):
if kind == "cat":
return "야옹"
elif kind == "dog":
return "멍멍"
elif kind == "cow":
return "음메"
print([make_sound_bad(k) for k in ["cat", "dog", "cow"]])
print("after는 7-1처럼 객체가 스스로 말한다")
['야옹', '멍멍', '음메']
after는 7-1처럼 객체가 스스로 말한다
make_sound_bad는 종류를 if 사슬로 늘어놓는다. 여기에 말을 추가하려면 elif를 한 줄 더 끼워 넣어야 하고, 그때마다 이미 잘 돌던 함수를 건드리게 된다. 앞의 07.1 방식은 이 분기가 아예 없다. 각 객체가 자기 speak()를 들고 있으니, 새 동물은 클래스만 하나 새로 만들면 되고 기존 코드는 손대지 않는다. 타입마다 늘어선 if가 보이면 다형성으로 바꾸라는 신호다.
07.3 상속 없이도 다형성, 덕 타이핑
지금까지의 클래스들은 공통 부모조차 없었다. 그래도 함께 다뤄졌다. 이것이 덕 타이핑이다. PDF, HTML, Markdown 세 클래스는 서로 아무 상속 관계가 없지만, 셋 다 render()를 갖고 있으니 하나의 함수로 똑같이 처리된다.
class PDF:
def render(self):
return "PDF 출력"
class HTML:
def render(self):
return "HTML 출력"
class Markdown:
def render(self):
return "MD 출력"
def print_all(docs):
return [d.render() for d in docs]
print(print_all([PDF(), HTML(), Markdown()]))
['PDF 출력', 'HTML 출력', 'MD 출력']
print_all은 넘어온 객체가 무슨 클래스인지 묻지 않는다. 그저 d.render()를 부를 뿐이다. 파이썬은 그 객체에 render라는 메서드가 있는지만 본다. 있으면 부르고, 없으면 그제야 에러를 낸다. 자바 같은 언어라면 공통 부모나 인터페이스로 세 클래스를 묶어야 했겠지만, 파이썬은 그런 억지 묶음 없이도 같은 메서드 이름만으로 다형성을 얻는다. 이 유연함이 덕 타이핑의 힘이다.
07.4 파이썬 내장 기능도 덕 타이핑이다
덕 타이핑은 내가 만든 클래스에만 해당하는 이야기가 아니다. 파이썬 내장 기능 자체가 이 방식으로 만들어져 있다. 내 클래스에 약속된 이름의 메서드를 정의하면, len() 같은 내장 함수가 그 객체에도 통한다.
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
pl = Playlist(["a", "b", "c"])
print("곡 수:", len(pl))
곡 수: 3
len(pl)은 내부적으로 pl.__len__()을 부른다. 파이썬은 pl이 무슨 타입인지 따지지 않고, __len__이라는 약속된 메서드가 있으면 그것을 실행한다. 이렇게 밑줄 두 개로 둘러싼 약속된 이름들을 프로토콜이라 부른다. 내 객체가 이 약속만 지키면 파이썬 문법과 내장 함수가 자연스럽게 맞물려 돌아간다. 덕 타이핑은 파이썬 밑바닥까지 관통하는 설계 원리다.
07.5 다형성으로 총합 계산
다형성이 실무에서 어떻게 코드를 줄이는지 한 예로 마무리한다. 원과 사각형이 뒤섞인 리스트에서 넓이의 합을 구한다. 각 도형의 area()만 부르면 되므로 종류를 구분할 필요가 없다.
class Circle2:
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
class Rect2:
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
shapes = [Circle2(1), Rect2(2, 3), Circle2(2)]
print("총 넓이:", round(sum(s.area() for s in shapes), 2))
총 넓이: 21.71
sum(s.area() for s in shapes)는 각 도형이 원인지 사각형인지 한 번도 묻지 않는다. area()라는 공통 약속만 믿고 값을 더한다. 삼각형을 추가하고 싶으면 area()를 가진 클래스를 새로 만들어 리스트에 넣기만 하면 된다. 합을 구하는 이 코드는 한 글자도 바뀌지 않는다. 종류를 묻지 않는 코드가 종류가 늘어도 흔들리지 않는 코드다.
07.6 정리
다형성은 같은 이름의 호출이 객체마다 다르게 동작하는 성질이고, 덕분에 종류를 따지는 if 분기를 없앨 수 있다. 파이썬은 상속 관계가 없어도 필요한 메서드만 있으면 같이 다루는 덕 타이핑을 쓴다. render()든 area()든 len()이든, 부르는 쪽은 약속된 이름만 알면 되고 실제 동작은 각 객체가 정한다. 새 종류가 늘어도 기존 코드를 건드리지 않는 이 성질이 다형성의 진짜 값어치다. 오늘 본 다섯 예제를 직접 쳐 보며 같은 호출이 객체마다 어떻게 갈라지는지 확인해 보기 바란다.