Python에서 인터페이스를 구현하는 방법에는 여러 가지가 있지만 이번 글에서는 Protocol을 이용한 방법을 소개한다.
인터페이스란?
쉽게 말해 외부와 소통하기 위해 필요한 메서드를 정의한것이다. 하위 모듈들은 해당 인터페이스에 맞춰 기능을 구현한다.
Protocol 사용법
아래 코드를 보자.
from typing import Protocol
class 감정(Protocol):
def 기쁘다(self) -> str:
...
def 슬프다(self) -> str:
...
class 사람:
def 기쁘다(self) -> str:
return "기뻐!"
def 화나다(self) -> str:
return "화나!"
class 사회생활:
def 시작(self, 사람: 감정) -> None:
self.사람 = 사람
def 기쁜일(self) -> str:
return self.사람.기쁘다()
def 화날일(self) -> str:
return self.사람.화나다()
갓생 = 사회생활()
갓생.시작(사람())
print(갓생.기쁜일())
print(갓생.화날일())
‘감정’이라는 클래스에 Protocol을 상속하고 필요한 메서드를 명시한다.
그다음 ‘사람’, ‘사회생활’이라는 클래스를 만들어 ‘갓생을 시작하는 사람’ 객체를 만든다. 이때 '사람'에게 Protocol을 적용하기 위해서 ‘감정’을 타입 힌트로 명시한다.
def 시작(self, 사람: 감정) -> None:
이제 기쁜일과 화날 일을 각각 호출해보자.
>>> 기뻐!
>>> 화나!
‘기뻐!’와 ‘화나!’가 성공적으로 출력된다.
읭? 뭔가 이상하다.
Protocol인 ‘감정’ 클래스에는 ‘화나다’라는 메서드가 없는데?
Protocol은 ABC 추상 메서드와 다르게 런타임 체크를 하지 않는다. 만약 추상 메서드를 상속받아 실행했다면 ‘슬프다' 메서드가 없다는 에러를 받았을 것이다. ('슬프다' 메서드를 구현하지 않았으므로)
대신 Protocol은 '정적 타입 검사'로 확인할 수 있다!
정적 타입 검사 툴 mypy로 확인해보자!
error: "감정" has no attribute "화나다"
error: Argument 1 to "시작" of "사회생활" has incompatible type "사람"; expected "감정"
note: "사람" is missing following "감정" protocol member:
note: 슬프다
에러가 거하게 발생했다. 하나씩 살펴보자.
- ‘감정’에는 ‘화나다’ 라는 속성이 없다고 한다.
- ‘사회생활’을 ‘시작’한 ‘사람’이 ‘감정’과 호환되지 않는다고 한다. ('감정'에는 '화나다' 메서드가 없으므로)
- ‘사람’에게는 ‘슬프다’ protocol member가 없다고 한다. ('사람'에는 '슬프다' 메서드가 없으므로)
우리는 위에처럼 정적 검사를 통해 해당 Protocol을 준수하고 있는지 확인할 수 있다.
Protocol 특징
ABC 추상클래스 같은 경우 상속을 통해 구현하지만 Protocol은 하위 모듈과 직접적으로 결합하지 않기 때문에 보다 유연하게 사용할 수 있다.
아래 코드를 보자.
from typing import Protocol
class 감정(Protocol):
def 기쁘다(self) -> str:
...
def 슬프다(self) -> str:
...
class 이성(Protocol):
def 맞다(self) -> str:
...
def 틀리다(self) -> str:
...
class 사람:
def 기쁘다(self) -> str:
return "기뻐!"
def 슬프다(self) -> str:
return "슬퍼!"
def 맞다(self) -> str:
return "맞아!"
def 틀리다(self) -> str:
return "틀려!"
class 사회생활:
def 시작(self, 사람: 감정) -> None:
self.사람 = 사람
def 기쁜일(self) -> str:
return self.사람.기쁘다()
def 틀린일(self) -> str:
return self.사람.틀리다()
갓생 = 사회생활()
갓생.시작(사람())
print(갓생.기쁜일())
print(갓생.틀린일())
이번에는 ‘이성’ Protocol을 추가하였다. 사람에는 ‘감정’과 ‘이성’ 메서드를 모두 구현하였다.
스크립트를 실행해보면 이번에도 아무 이상 없이 출력된다.
>>> 기뻐!
>>> 틀려!
mypy로 정적 타입 검사를 해보자.
error: "감정" has no attribute "틀리다"
‘감정’에는 ‘틀리다’라는 속성이 없다고 한다.
‘틀리다’는 ‘이성’의 메서드이지만 ‘사람’은 '감정' Protocol을 따르므로 발생한 에러이다.
이번에는 ‘감정’ 대신 ‘이성’을 타입힌트로 명시해보자!
class 사회생활:
def 시작(self, 사람: 이성) -> None:
self.사람 = 사람
스크립트는 그대로 잘 실행되니 제외하고, mypy로 정적 타입 검사를 해보자.
error: "이성" has no attribute "기쁘다"
이번에는 ‘이성’에 ‘기쁘다’라는 속성이 없다고 한다.
이처럼 Protocol은 타입 힌트를 변경하는 것만으로 해당하는 인터페이스를 교체할 수 있다.
그래서 Protocol이 뭐가 유연한 거야?라는 의문이 들지도 모르겠다. 위에 코드는 일부러 에러를 발생시키기 위해 만든 코드이다. 만약 ‘이성’이 필요한 ‘사회생활’, ‘감정’이 필요한 ‘연애생활’을 구분해서 Protocol을 사용한다면 동일한 ‘사람’ 객체를 넣고도 모두에 활용할 수 있다.
정리
Protocol은 상속 없이 타입 힌트만으로 인터페이스 역할을 수행할 수 있다.
Protocol은 상속하지 않기 때문에 더 유연하게 활용할 수 있다.
Protocol은 런타임이 아닌 정적 타입 검사를 이용해 오류를 체크할 수 있다.
Protocol은 런타임 체크 기능이 없기 때문에 상대적으로 비용이 적다.
레퍼런스
'나는 이렇게 학습한다 > Language' 카테고리의 다른 글
JavaScript _ 콜백 함수를 알아보자 (0) | 2023.05.09 |
---|---|
Python _ zoneinfo 사용법, ZoneInfo 와 pytz 차이 (0) | 2022.10.27 |
Python _ TypedDict를 사용하는 이유(feat. mypy) (0) | 2022.06.22 |
Python _ dict의 keys()처럼 dataclass에서 속성 목록 가져오기 (0) | 2022.06.21 |
Python _ @dataclass 사용법과 타입 확인 (0) | 2022.05.18 |