← notes

Python - Interface, Protocols, ABCS

2024-09-28 · python

[!TIP] Source 전문가를 위한 파이썬(Fluent Python) 13장

4가지 타이핑 유형

![[python-typings.png|475]]

이번 챕터를 그림 한장에 🦆🪿🤯

이 그림 보기 전에 우선 타입 검사에 대해서 한번 짚고 넘어가자. Python에서 가능한 타입 검사(Type Checking)은,

def add(x: int, y: int) -> int: #int라고 type hinting
    return x + y

print(add(3, 5))   # 정상 작동
print(add("3", "5"))  # mypy 검사 시 오류 발생
$ mypy script.py
error: Argument 1 to "add" has incompatible type "str"; expected "int"

다시 그림으로 돌아가서 축을 보면,

이 기준들로 볼 경우 4가지 타이핑 유형이 나눠진다는 것

사실 정적 타이핑은 명확하기 때문에… 덕타이핑, 구스타이핑, 정적 덕 타이핑(정적 프로토콜)은 아래에서 더 자세히 다룬다!


프로토콜이란?


덕 타이핑 (=동적 프로토콜)

멍키 패칭

기존 코드(클래스, 모듈, 라이브러리 등)를 수정하지 않고, 런타임에 동적으로 속성(메서드, 변수 등)을 변경

class Dog:
    def speak(self):
        return "멍멍!"

dog = Dog()
print(dog.speak())  # 멍멍

# 멍키 패칭
def new_speak(self):
    return "월월!"
Dog.speak = new_speak 

print(dog.speak())  # 월월

방어적 프로그래밍/조기 실패

방어운전처럼, 부주의한 프로그래머가 있더라도 안정성을 높이는 관례

def __init__(self, iterable):
	self._balls = list(iterable)
def multiply(x: int, y: int) -> int:
    return x * y

print(multiply("3", 4))  # "3333"
#mypy를 쓰면 "3"이 int가 아니라고 경고, 하지만 런타임에서 그냥 실행은 되어버림


def multiply(x: int, y: int) -> int:
    if not isinstance(x, int) or not isinstance(y, int):
        raise TypeError("x와 y는 정수여야 합니다.")  #조기실패
    return x * y

구스 타이핑 (ABC)

추상 베이스 클래스(ABC)로 인터페이스를 정의하고 상속/등록한 뒤 런타임 검사(isinstance, issubclass)하기!

class Dog:
    def speak(self):
        return "멍멍!"

class Cat:
    def speak(self):
        return "야옹!"

def make_sound(animal):
    if isinstance(animal, Dog) or isinstance(animal, Cat): 
        return animal.speak()
    else:
        raise TypeError("Animal이 아닙니다!")
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass
        
    def sleep(self):
        print("zzz...") 

class Dog(Animal):
    def speak(self):
        return "멍멍!"

class Cat(Animal):
    def speak(self):
        return "야옹!"

def make_sound(animal: Animal):
    if isinstance(animal, Animal):
        return animal.speak()
    else:
        raise TypeError("Animal이 아닙니다!")

표준 라이브러리 ABC

![[collections_abc.png|500]]

ABC 정의하고 상속하기

아이템을 랜덤하게 보여주지만 목록에 있는 아이템을 다 보여줄 때까지 같은 아이템을 반복해서 보여주면 안 됨

import abc

class Tombola(abc.ABC):

	@abc.abstractmethod
	def load(self, iterable):
		"""iterable의 항목들을 추가한다."""

	@abc.abstractmethod
	def pick(self):
		"""무작위로 항목 하나를 제거하고 반환한다.
		인스턴스가 비어 있으면 LookupError를 발생시킨다.
		"""

	def loaded(self):
		"""항목이 최소 한 개 이상 있으면 True, 아니면 False 반환한다."""
		return bool(self.inspect())

	def inspect(self):
		"""현재 항목들로 정렬된 튜플을 만들어 반환한다."""
		items = []
		while True:
			try:
				items.append(self.pick())
			except LookupError:
				break
		self.load(items)
		return tuple(items)
class Fake(Tombola):
	def pick(self):
		return 13

f = Fake() #TypeError

ABC를 상속하지 않고 가상 서브클래스로 등록하기

class Car:
    pass 
Animal.register(Car) #이렇게 해도 되고

@Animal.register
class Car:
	pass #이렇게 해도 됨


####

issubclass(Car, Animal) #True

car = Car() #인터페이스 구현을 안했는데도 잘 생성됨
print(isinstance(car, Animal))  #True

#하지만 abstractmethod인 speak을 구현하지 않았기 때문에..
print(car.speak())  # AttributeError: 'Car' object has no attribute 'speak'

정적 덕 타이핑 (=정적 프로토콜)

typing.Protocol 을 사용해서 특정 인터페이스(메서드/속성)만 가지만 해당 타입으로 인정하는 방식

from typing import Protocol

class Flyable(Protocol): 
    def fly(self) -> str:
        ...

class Bird:
    def fly(self) -> str:
        return "새가 난다!"

class Airplane:
    def fly(self) -> str:
        return "비행기가 난다!"

class Fish:
    def swim(self) -> str: 
        return "물고기가 헤엄친다!"

def make_it_fly(flyer: Flyable):
    print(flyer.fly()) 

bird = Bird()
plane = Airplane()
fish = Fish()

make_it_fly(bird) 
make_it_fly(plane)
make_it_fly(fish)   # 오류 발생
isinstance(bird, Flyable) #TypeError. Flyable은 런타임에서 클래스로 인식X
from typing import Protocol, runtime_checkable

@runtime_checkable
class Flyable(Protocol):
    def fly(self) -> str:
        ...

class Bird:
    def fly(self) -> str:
        return "새가 날아갑니다!"

class Fish:
    def swim(self) -> str:
        return "물고기가 헤엄칩니다!"

b = Bird()
f = Fish()

print(isinstance(b, Flyable))  # True (Bird는 fly()를 구현했으므로)
print(isinstance(f, Flyable))  # False (Fish는 fly() 없음)

검사 방식검사 시점타입 체크 방식상속 필요 여부isinstance() 사용 가능강제되는 메서드
동적 타입 검사런타임isinstance() 등으로 직접 검사❌ (아무 메서드나 가능)
정적 타입 검사정적 검사기(mypy)타입 힌트 기반 검사❌ (힌트만 제공)
ABC 상속런타임isinstance() 검사 가능✅ (추상 메서드 구현 필수)
ABC register()런타임isinstance() 검사 가능 (등록된 클래스만)❌ (구현 강제 안 함)
Protocol (typing.Protocol)정적 검사기(mypy)“해당 메서드만 있으면 타입 인정”🔺 (데코레이터 사용하면 가능)✅ (정의된 메서드 필요)