도비LOG(跳飛錄)

도비의 AI 엔지니어 도전기

Python

[전문가를 위한 파이썬] 11장 인터페이스: 프로토콜에서 ABC까지

나쁜도비 2024. 8. 31. 13:42

11.1. 파이썬 문화에서의 인터페이스와 프로토콜

- 인터페이스: 시스템에서 어떤 역할을 할 수 있게 해주는 객체의 공개 메서드의 일부

- 프로토콜: 어떤 역할을 완수하기 위한 메서드 집합으로서의 인터페이스

 

 

11.2. 파이썬은 시퀀스를 찾아낸다

- 파이썬은 __gititem__() 메서드만 구현하더라도 __gititem__() 메서드를 호출해서 객체를 반복하고 in 연산자를 사용할 수 있게 해준다.

 

 

11.3. 런타임에 프로토콜을 구현하는 멍키 패칭

import collections
from random import shuffle

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]


def set_card(deck, position, card):
  deck._cards[position] = card

FrenchDeck.__setitem__ = set_card # set_card 함수를 FrenchDeck 클래스의 __setitem__이라는 이름의 속성에 할당한다.

deck = FrenchDeck()
shuffle(deck) # 이제 FrenchDeck 클래스가 가변 시퀀스 프로토콜에 필요한 메서드를 구현하므로, deck을 섞을 수 있다.
deck[:5]

- deck 객체에 _cards라는 이름의 속성이 있고, _cards가 가변 시퀀스임을 set_card()가 알고 있다는 것이 비결.

- set_card() 함수는 FrenchDeck 클래스의 __setitem__ 특별 메서드에 연결된다. 이 방법은 멍키 패칭(monkey patching)의 한 예이다.

- 멍키 패칭: 소스코드를 건드리지 않고 런타임에 클래스나 모듈을 변경하는 행위

 

 

11.4. 알렉스 마르텔리의 물새

- 덕타이핑은 객체의 실제 자료형은 무시하고, 대신 객체가 용도에 맞는 메서드 이름, 시그너처, 의미를 구현하도록 보장하는 데 주안점을 둔다.

- 저자가 제안하는 구스 타이핑: cls가 추상 베이스 클래스인 경우, 즉 cls의 메타클래스가 abc.ABCMeta인 경우에는 isinstance(obj, cls)를 쓴다.

 

class Struggle:
  def __len__(self): return 23

from collections import abc
isinstance(Struggle(), abc.Sized)

# True

- abc.Sized 클래스는 Struggle을 일종의 '서브클래스'로 인식한다.

- numbers, collections.abc 등의 ABC가 표현하는 개념을 실현하는 클래스를 구현할 때는 언제나 해당 ABC를 상속하거나 해당 ABC에 등록하라.

 

 

11.5. ABC 상속하기

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]

  def __setitem__(self, position, value):
    self._cards[position] = value

  def __delitem__(self, position):
    del self._cards[position]

  def insert(self, position, value):
    self._cards.insert(position, value)

- 파이썬은 모듈을 로딩하거나 컴파일할 때가 아니라, 실행 도중 실제로 FrenchDeck2 객체를 생성할 때 추상 메서드의 구현 여부를 확인한다.

 

 

11.6. 표준 라이브러리의 ABC

11.6.1. collections.abc의 ABC

collections.abc에 정의된 ABC 주요 속성

- Iterabble, Container, Sized: Iterale은 __iter__()를 통해 반복을, Container는 __contains__()를 통해 in 연산자를, Sized는 __len__()을 통해 len() 메서드를 지원한다.

- Sequence, Mapping, Set: 주요 불변 컬렉션형. 각각 가변형 서브클래스가 있다.

- MappingView: items(), keys(), values() 메서드에서 반환된 객체는 각각 ItemView, KeysView, ValuesView를 상속한다.

- Callable, Hashable: 주로 어떤 객체를 호출하거나 해시할 수 있는지 안전하게 판단하기 위해 isinstance() 함수와 함께 사용된다.

- Iterator: Iterable을 상속한다.

 

11.6.2. ABC의 숫자탑

- numbers 패키지는 '숫자탑'을 정의한다. Number가 최상위 슈퍼클래스이며, 그 다음으로 Complex, Real, Rational, Integral 순으로 내려간다.

 

 

11.7. ABC의 정의와 사용

- 상황 가정: 광고를 무작위순으로 보여주되, 목록에 들어있는 광고를 모두 보여주기 전까지는 같은 광고를 반복하지 않는다.

import abc
class Tombola(abc.ABC): # ABC 상속

  @ abc.abstractmethod
  def load(self, iterable): # 데커레이터
    """iterable 객체에 있는 항목들을 추가"""

  @ abc.abstractmethod
  def pick(self): 
    """무작위로 항목을 하나 제거하고 반환.
    객체가 비어있을 때 이 메서드를 실행하면 `LookupError` 발생"""

  def loaded(self):
    """최소 1개의 항복이 있으면 True, 그렇지 않으면 False 반환"""
    return bool(self.inspect()) # ABC의 구상 메서드는 반드시 ABC에 정의된 인터페이스(즉, ABC읟 ㅏ른 구상 메서드나 추상 메서드, 혹은 프로퍼티)만 사용해야 한다.

  def inspect(self):
    """현재 안에 있는 항목들로 구성된 정렬된 튜플 반환"""
    items = []
    while True: 
      try:
        items.append(self.pick())
      except LookupError:
        break
    self.load(items) # 
    return tuple(sorted(items))

 

11.7.1. ABC 상세 구문

- ABC를 선언할 때는 abc.ABC나 다른 ABC를 상속하는 방법이 가장 좋다.

 

11.7.2. Tombola ABC 상속하기

- Tombola ABC의 인터페이스를 만족시키는 구상 서브클래스를 만들어보자.

import random
class BingoCage(Tombola): # Tombola 상속
  def __init__(self, items):
    self._randomizer = random.SystemRandom()
    self._items = []
    self.load(items)

  def load(self, items):
    self._items.extend(items)
    self._randomizer.shuffle(self._items)

  def pick(self):
    try:
      return self._items.pop()
    except IndexError:
      raise LookupError('pick from empty BingoCage')

    def __call__(self):
      self.pick()
import random
class LotteryBlower(Tombola):
  def __init__(self, iterable):
    self._balls = list(iterable)

  def load(self, iterable):
    self._balls.extend(iterable)

  def pick(self):
    try:
      position = random.randrange(len(self._balls))
    except ValueError:
      raise LookupError('pick from empty LotteryBlower')
    return self._balls.pop(position) # 무작위로 선택된 항목 꺼내기

  def loaded(self): # inspect()를 호출하지 않도록 loaded() 메서드를 오버라이드한다.
    return bool(self._balls)

  def inspect(self):
    return tuple(sorted(self._balls)) # inspect() 오버라이드

 

11.7.3. Tombola의 가상 서브클래스

- 구스 타이핑의 본질적인 기능은 어떤 클래스가 ABC를 상속하지 않더라도 그 클래스의 가상 서브클래스로 등록할 수 있다는 것이다.

- Tombola의 가상 서브클래스 TomboList

from random import randrange

@Tombola.register # TomboList를 Tombola의 가상 서브클래스로 등록한다.
class TomboList(list): # list를 상속한다.
  def pick(self):
    if self: # list에서 __bool__이 상속된다. 리스트가 비어있지 않으면 True를 반환한다.
      position = randrange(len(self))
      return self.pop(position) # pick() 메서드는 무작위 인덱스를 전달해서 list에서 상속한 self.pop()을 호출한다.
    else:
      raise LookupError('pop from empty TomboList')

    load = list.extend # list.extend() 메서드를 TomboList.load에 할당한다.
    
  def loaded(self):
    return bool(self) # loaded() 메서드를 bool() 함수에 위임한다.

  def inspect(self):
    return tuple(sorted(self))

Tombola.register(TomboList)

- TomboList를 Tombola 클래스의 가상 서브클래스로 등록했기 때문에 이제 issubclass()와 isinstance() 함수는 TomboList가 Tombola의 서브클래스인 것처럼 판단한다.

 

 

11.8. Tombola 서브클래스 테스트 방법

- __subclasses__(): 클래스의 바로 아래 서브클래스의 리스트를 반환하는 메서드. 리스트에 가상 서브클래스는 들어가지 않는다.

- _abc_registry: ABC에서만 사용할 수 있는 데이터 속성으로, 추상 클래스의 등록된 가상 서브클래스에 대한 약한 참조를 담고 있는 WeakSet이다.

 

 

 

728x90