티스토리 뷰
Reference
- Fluent Python
- docs.python.org
- Stackoverflow
- Wikipedia
Interface
객체지향 프로그래밍에서 인터페이스는 객체가 "X"라는 타입이 되기 위해
꼭 가져야 할 메서드들의 집합이다.
사례를 통해 인터페이스에 대해 알아가 보자
간단하게 Bike를 생각해보자
Bike가 되기 위해 꼭 있어야 할게 무엇인가?
일단 움직이기 위해 바퀴가 있어야 하고
움직일 줄 알아야 하며
멈출 줄도 알아야 한다.
이런 부분들은 정말 Bike에게 필수적인 부분들이다.
만약 저 세개중에 하나라도 없다면
그게 Bike라고 할 수 있을까?
인터페이스의 목적은 위 사례를 보았듯이
만약 "Bike"라는 타입이 있으면
그 타입이 되기 위한 객체들에게 꼭 필요한 속성들을 강제하고
특정 타입에는 이런 게(동작, 속성) 꼭 필요하단 걸 파악하는 것이다.
그렇다면 이런 인터페이스를 파이썬에서 구현할 수 있을까?
물론 ABC(Abstract Base Class) 모듈을 통해 위에서 설명한 인터페이스처럼 강제할 순 있다.
하지만 파이썬 같은 동적 타입 언어에는 인터페이스가 필요 없다.
왜 파이썬에는 인터페이스가 필요 없을까?
우리가 보통 다형성을 구현할 때
Java 같은 언어는 위에서 봤던 방식대로
인터페이스 구현이나 상속을 통해 미리 타입을 결정한다.
정적 타입 언어다운 방식이다.
하지만 파이썬은 동적 타입 언어이다.
파이썬은 객체의 타입을 동적으로 결정한다.
Duck Typing
class MotorBike:
def accelerate(self):
print("오토바이가 액셀을 당깁니다.")
def stop(self):
print("오토바이 브레이크를 당깁니다.")
class CityBike:
def accelerate(self):
print("자전거 페달을 밟습니다.")
def stop(self):
print("자전거 브레이크를 당깁니다.")
class Bird:
def flying(self):
print("새가 날아다닙니다.")
def red_light(bike):
bike.stop()
red_light(MotorBike())
red_light(CityBike())
>>> 오토바이 브레이크를 당깁니다.
>>> 자전거 브레이크를 당깁니다.
위 코드를 보면
red_light라는 함수가 있는데
매개변수로 받은 객체의 stop() 메서드를 호출한다.
오토바이(MoterBike)와 자전거(CityBike)는 stop메서드가 구현되어 있기 때문에
red_light매개변수의 인수로 넘겨줘도 에러가 발생하지 않고 잘 실행된다.
하지만 stop메서드가 없는 Bird객체를 넘겨주면
red_light(Bird())
>>> AttributeError: 'Bird' object has no attribute 'stop'
Bird객체에는 stop이라는 어트리뷰트가 없다고
AttributeError가 발생한다.
위 사례를 보면
우린 red_light에 넘어온 객체가 어떤 타입인지 전혀 검사하지 않고
그냥 그 객체가 stop이라는 메서드만 구현되어 있으면 통과시켰다.
(만약 Java 같은 정적 타입 언어는 객체가 어떤 타입인지 먼저 검사했을 것이다.)
이런 방식은 파이썬이 동적 타입 언어이기에 가능한 방식이고
이런 타이핑 방식을 덕 타이핑이라고 한다.
<혹시 덕 타이핑이 잘 이해가 되지 않다면 이 글을 참고하기 바란다.>
https://stackoverflow.com/questions/4205130/what-is-duck-typing
What is duck typing?
What does duck typing mean in software development?
stackoverflow.com
만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
-덕 타이핑(위키백과)-
위에서 봤듯이
덕 타이핑을 이용해서
우린 인터페이스와 상속 없이 다형성을 구현했다.
위에서 필자는 인터페이스에 대해 이렇게 정의했다.
객체지향 프로그래밍에서 인터페이스는
객체가 "X"라는 타입이 되기 위해 꼭 가져야 할 메서드들의 집합이다.
왜 파이썬에는 인터페이스가 필요 없는지 이해가 되지 않는가?
파이썬은 인터페이스를 통해 미리 객체의 타입을 결정할 필요가 없다.
파이썬은 타입을 동적으로 결정할 수 있다.
그냥 객체에 그런 메서드가 있으면
그런 타입으로 간주하겠다(동적으로 결정)는 개념이다.
(객체가 용도에 맞는 메서드 이름, 시그너처, 의미를 구현하도록 보장하는데 주안점을 둔다.)
위에서 봤던 stop메서드를 우린 객체들에게 강제하지 않았다.
하지만 이런 stop메서드가 있어야만
red_light라는 함수에 인수로 실행될 수 있는 조건과도 같다.
이런 메서드들의 집합들을 동적 언어에서는 프로토콜이라고 부른다.
Protocol
시퀀스 프로토콜을 보면
다양한 메서드들을 구현해야 하지만
필자는 __getitem__ 매직 메서드 하나만 구현해보겠다.
class Test:
def __getitem__(self, item):
return range(0, 101, 2)[item]
x = Test()
print(x[20])
>>> 40
for i in x:
print(i)
>>> 0, 2, 4, 6, 8 ...
print(30 in x)
>>> True
print(500 in x)
>>> False
Test에 우린 __getitem__메서드 하나만 정의했다.
우린 __iter__와 __contain__메서드를 정의해주지 않았음에도 불구하고
Test객체는 인덱스를 통해 항목들을 탐색할 수 있으며
in 연산자와 반복이 가능한 객체가 되어있다.
이게 어떻게 가능한 걸까?
답은 Fluent Python에 나온 바에 의하면
프로토콜의 중요성 때문이라고 한다.
우리가 보통 인덱스를 통해 항목들을 탐색하려고 하려면
__getitem__매직 메서드를 정의한다.
파이썬 인터프리터는 0부터 시작하는 정수 인덱스로 __getitem__메서드를 호출하여
객체 반복을 시도한다.
그렇다면 이미 그 객체는 반복 가능하다는 게 아닐까?
하나하나 값을 확인하는 게 가능하다면 당연히 in연산도 작동할 것이다.
(하나하나 돌리면서 확인해보면 되기 때문)
파이썬 공식문서에 시퀀스 프로토콜 부분 PySequence_Check함수를 보면 알 수 있듯이
__getitem__() 메서드가 있는 파이썬 클래스의 경우는
dict 서브 클래스가 아닌 한 객체가 시퀀스 프로토콜을 제공한다고 볼 수 있다.
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
x = FrenchDeck()
Fluent Pythonp에 있는 FrenchDeck클래스를 보면
__len__, __getitem__메서드가 정의되어있는 걸 볼 수 있다.
모든 Sequence 프로토콜을 정의하진 않았지만
우린 이 FrenchDeck객체가 부분적으로 시퀀스처럼 작동하는 걸 알 수 있다.
만약 우리가 이 FrenchDeck클래스에 카드를 섞는 기능을 추가한다고 가정하면
우린 FrenchDeck객체가 시퀀스처럼 동작한다는 걸 알고 있기 때문에
shuffle() 메서드를 직접 구현할 필요 없이
내장 모듈 random.shuffle() 함수를 가져오면 된다.
그럼 이제 random모듈을 통해서 shuffle함수로 카드를 섞어보겠다.
from frenchdeck import FrenchDeck
from random import shuffle
deck = FrenchDeck()
shuffle(deck)
정상적으로 잘 실행될 것이라고 예상했지만
에러가 발생했다.
File "....pyenv/versions/3.9.4/lib/python3.9/random.py",
line 363, in shuffle x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment
에러 메시지 두 번째 줄을 보면
x[i], x[j] = x[j], x[i]
random.shuffle() 메서드는 시퀀스 안의 항목들의 위치를 교환시킨다는 걸 알 수 있다.
하지만 우리의 FrenchDeck객체는 __setitem__매직 메서드가 없기 때문에
할당을 지원하지 않는 것이고 그래서 에러가 발생한 것이다.
(불변 상태)
random.shuffle() 함수는 우리가 위에서 봤던 덕 타이핑 예제처럼
__setitem__메서드가 지원되는
가변 시퀀스에게만 동작이 허락된다.
여기서 __setitem__은 자연스럽게 가변 시퀀스의 프로토콜이란 걸 알 수 있다.
해결책은 간단하다 FrenchDeck에 __setitem__을 정의해서
부분적인 가변 시퀀스로 만들어주면 된다.
# frenchdeck.py
def __setitem__(self, key, value):
self._cards[key] = value
# shuffle.sequence.py
from frenchdeck import FrenchDeck
from random import shuffle
deck = FrenchDeck()
print(deck[:5])
>>>[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades'), ...
shuffle(deck)
print(deck[:5])
>>>[Card(rank='7', suit='spades'), Card(rank='A', suit='clubs'), Card(rank='3', suit='clubs'), ...
이제는 잘 동작되는 걸 확인할 수 있다.
random.shuffle함수는 자신이 받은 인수의 자료형에 대해 하나도 검사하지 않고
그저 그 인수가 가변 시퀀스 프로토콜(__setitem__)을 구현하고 있으면 될 뿐이다.
위 예제는 덕 타이핑과 프로토콜에 대해 잘 보여준 예제가 아닌가 싶다.
구스 타이핑? ABC?
덕 타이핑을 통해
파이썬에서 객체의 실제 자료형보단
객체가 사용되는 용도에 맞게 구현하는 것에 대해 중점을 두고 있다는 걸 알게 되었다.
(isinstance() 메서드 사용을 통한 자료형 검사를 회피)
그렇다면 ABC(Abstract Base Class)는 필요 없는 걸까?
어쩌면 ABC는 추상 메서드를 통해
해당 타입이 되기 위한 메서드를 강제한다.
지금까지 배운 내용을 토대로 보면
파이썬은 뭔가 타입을 강제하는 것과는
거리가 멀다는 느낌이 든다.
덕 타이핑이 상당히 유용하게 사용되는 상황이 많지만
만약 그렇지 않은 상황에서는
어떻게 해야 할까?
이러한 해결책으로
덕 타이핑을 조금 더 보완한 형태인
구스 타이핑이라는 게 등장한다.
구스 타이핑에 대한 건 다음 포스트에 작성하도록 하겠다.
후기
덕 타이핑은 굉장히 중요한 주제인 거 같다.
사실 필자는 이해하기 조금 힘들었다.
덕 타이핑의 논리대로라면
내가 꽥꽥거리면 나도 오리가 되는 건가 싶었다.
극단적인 사례를 통하면 이해하기가 오히려 어려웠다.
단순히 덕 타이핑은 어쩌면 파이썬 같은 동적 타입 언어가 지향하는 방향일 뿐이다.
(상식적으로 이해가 가지 않았다)
객체가 용도에 맞게 구현될 수 있도록
메서드 이름이나 의미에 대해
많은 중점을 두겠다는 것이다.
이런 글을 후기에도 주절주절 적는 걸 보니
나도 아직 완벽히 이해를 하지 못한 걸까?
라는 생각이 든다.
![](https://t1.daumcdn.net/keditor/emoticon/niniz/large/019.gif)
너무 어렵게 생각하지 말자
하나의 다른 세계와 같다.
어쩌면 프로그래밍할 때는
일상생활에서 사용했던 두뇌가 아닌
다른 두뇌를 꺼내와야 하는 게 아닌가 싶다.
'Python' 카테고리의 다른 글
[파이썬] 코루틴 (0) | 2022.07.11 |
---|---|
[파이썬] 데코레이터 (0) | 2022.07.04 |
[파이썬]이터레이터, 이터러블, 제너레이터 (0) | 2022.06.24 |
[파이썬] 클로저(Closure) (0) | 2022.05.31 |
[파이썬] 함수가 가진 속성 파악하기 (0) | 2022.05.25 |