abc는 Python 2, 3 모두에서 사용할 수 있는 파이썬 표준 라이브러리 중 하나다. abc는 Abstract Base Class의 약자인데, 말 그대로 객체지향에서의 abstract 개념을 파이썬에서 잘 사용할 수 있도록 도와준다. PEP 3119에서 2007년 4월에 최초 제안되었다.

Java의 추상 메소드 이야기

OOP를 내세우는 언어들은 보통 추상 메소드가 있다. 나는 Java가 편하므로 Java를 예로 들겠다.

interface Hashable {
  abstract public int hash();
}

class Number implements Hashable {
  @Override
  public int hash() {
    return ...;
  }
}

abstract가 명시된 추상 메소드를 가진 추상 클래스, 혹은 인터페이스를 상속받은 일반 클래스는, 해당 추상 메소드를 모두 override하지 않으면 에러가 발생한다. 객체지향 5원칙인 SOLID 중 L인 리스코브 치환 원칙(LSP, Liskov Substitution Principle)에게 필요한 개념이다 뭐다 이런 이야기들을 하는데, 구체적인 것들은 이 글의 주제를 벗어나므로 직접 알아보기 바란다.

Python에서 추상 메소드 표현하기

abc 없이 하기

Python에는 abstract 키워드와 같은 언어 레벨의 추상 메소드 표현법이 따로 없다. abc가 세상에 널리 알려지기 전까지는, 추상 메소드를 표현하기 위해 메소드의 body에 raise NotImplementedError를 두는 방법을 사용하곤 했다.

class Abstract:
  def foo(self):
    raise NotImplementedError()

대표적인 old-school way이며, abc를 사용하는 것에 비해 허점이 많다.

abc의 도움을 받기

기본적으로, NotImplementedError를 사용하는 방식은 엄격하지 않다. 추상 메소드를 구현하지 않더라도, 메소드를 실제로 호출하기 전까지는 알아차릴 수 없다. 추상 메소드와 관련된 에러를 raise하는 시점이 런타임이라는 것이다.

만약 인스턴스화 시점, 클래스 정의 시점, 컴파일 타임 등 이보다 더 상위의 시점에 추상 메소드의 구현 여부를 확인할 수 있다면 더 좋을 것이다. abc.ABC를 상속받고, 원하는 메소드에 abc.abstractmethod 데코레이터를 달아주면 된다.

from abc import ABC, abstractmethod


class Abstract(ABC):
  @abstractmethod
  def foo(self):
    pass


class WithOverride(Abstract):
  def foo(self):
    pass


class WithoutOverride(Abstract):
  pass

WithOverride()
WithoutOverride()
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: Can't instantiate abstract class WithoutOverride with abstract methods foo

collections.abc

또 다른 표준 라이브러리인 collections의 서브 모듈 collections.abc에서 abc를 잘 쓰고 있다. 이 모듈은 어떤 클래스가 특정 기능을 지원하는지 여부를 테스트하는 데 써먹을 수 있는 추상 클래스를 제공한다. hashable, callable 여부 등을 예로 들 수 있다.

from collections.abc import Container, Hashable, Sized, Callable, MutableSequence

print(isinstance([], Container)) # True
print(isinstance([], Hashable)) # False
print(isinstance(0, Hashable)) # True
print(isinstance(0, Sized)) # False
print(isinstance('', Sized)) # True
print(isinstance(lambda: 0, Callable)) # True
print(isinstance([], MutableSequence)) # True
print(isinstance(set(), MutableSequence)) # False

이런저런 복잡한 것들을 제외하고 쉽게 왜곡해 설명하면, 메타클래스로 abc.ABCMeta를 상속받은 클래스를 비교 대상으로 하여 isinstance 호출 시 __subclasshook__ 메소드가 그 결과를 결정하게 된다. 예제로 살펴보자.

from abc import ABCMeta


class Object(metaclass=ABCMeta):
  @classmethod
  def __subclasshook__(cls, C):
    return True

print(isinstance(0, Object)) # True