Python에서 추상 클래스는 “상속만 가능한 불완전한 클래스”라기보다, 구현체가 지켜야 할 최소 계약을 실행 시점에도 확인하는 도구에 가깝다. abc.ABC를 상속하고 @abstractmethod를 붙이면, 구현되지 않은 추상 메서드가 남은 클래스의 인스턴스 생성을 막을 수 있다.
여기서 자주 놓치는 조건이 있다. ABC를 상속했다는 이유만으로 항상 인스턴스화가 금지되는 것은 아니다. 클래스의 __abstractmethods__에 구현되지 않은 항목이 남아 있을 때만 TypeError가 발생한다.
가장 작은 실행 예제
직렬화 형식을 예로 들면 계약과 공통 구현을 한 클래스에 둘 수 있다.
from abc import ABC, abstractmethod
from pathlib import Path
class Serializer(ABC):
@abstractmethod
def dumps(self, value: object) -> str:
"""객체를 문자열로 직렬화한다."""
raise NotImplementedError
def dump_file(self, value: object, path: Path) -> None:
path.write_text(self.dumps(value), encoding="utf-8")
class IncompleteSerializer(Serializer):
pass
class JsonSerializer(Serializer):
def dumps(self, value: object) -> str:
import json
return json.dumps(value, ensure_ascii=False)
try:
IncompleteSerializer()
except TypeError as error:
print(type(error).__name__)
serializer = JsonSerializer()
print(serializer.dumps({"name": "준희"}))
IncompleteSerializer에는 dumps()가 남아 있으므로 객체를 만들 수 없다. 반면 JsonSerializer는 계약을 충족하므로 인스턴스화할 수 있고, 부모가 제공한 dump_file()도 그대로 사용한다.
ABC와 abstractmethod의 역할은 다르다
ABC는ABCMeta를 사용하기 위한 편리한 base class다.@abstractmethod는 subclass가 구현해야 할 method를 표시한다.- 구현하지 않은 abstract method가 하나라도 남으면 그 subclass도 abstract class다.
- abstract method가 하나도 없다면
ABC를 상속한 class도 인스턴스화할 수 있다. - abstract class 안에는 일반 method, property, state를 함께 둘 수 있다.
따라서 “추상 클래스에는 구현을 넣을 수 없다”는 설명은 정확하지 않다. 계약은 추상 메서드로 표현하고, 모든 구현체가 공유할 안전한 동작은 concrete method로 제공할 수 있다.
추상 메서드에도 기본 구현을 둘 수 있다
@abstractmethod가 붙은 method body가 반드시 pass일 필요는 없다. 협력적 다중 상속처럼 하위 클래스가 super()로 공통 동작을 호출해야 하는 설계에서는 추상 메서드에도 구현을 둘 수 있다.
from abc import ABC, abstractmethod
class Normalizer(ABC):
@abstractmethod
def normalize(self, text: str) -> str:
return text.strip()
class LowerNormalizer(Normalizer):
def normalize(self, text: str) -> str:
return super().normalize(text).lower()
print(LowerNormalizer().normalize(" Hello "))
핵심은 method body의 유무가 아니라 decorator가 만든 abstract 상태다. 하위 클래스는 여전히 override해야 하며, 필요하면 부모 구현을 재사용한다.
property와 classmethod에 붙이는 순서
abstractmethod는 가장 안쪽 decorator로 둔다. property와 class method는 다음 순서로 선언한다.
from abc import ABC, abstractmethod
class Document(ABC):
@property
@abstractmethod
def media_type(self) -> str:
raise NotImplementedError
@classmethod
@abstractmethod
def from_text(cls, text: str) -> "Document":
raise NotImplementedError
반대 순서는 descriptor가 abstract flag를 올바르게 전달하지 못해 오류가 날 수 있다. 오래된 abstractclassmethod, abstractstaticmethod, abstractproperty 대신 이 조합을 사용한다.
virtual subclass는 일반 상속과 다르다
ABC는 register()로 virtual subclass를 등록할 수 있다.
from abc import ABC
class Payload(ABC):
def validate(self) -> bool:
return True
class LegacyPayload:
pass
Payload.register(LegacyPayload)
print(issubclass(LegacyPayload, Payload))
이 경우 issubclass()는 True지만, Payload의 method가 LegacyPayload의 MRO에 들어오지는 않는다. 등록만으로 abstract method 구현도 강제되지 않는다. virtual subclass는 runtime 분류가 필요한 제한된 상황에서 사용하고, 실제 behavior contract까지 보장한다고 해석하지 않는다.
ABC와 Protocol 중 무엇을 고를까
| 기준 | ABC | typing.Protocol |
|---|---|---|
| 관계 | 명시적 상속 중심 | 필요한 attribute 구조 중심 |
| 확인 시점 | 미구현 abstract method의 인스턴스화 차단 | type checker의 정적 검사 중심 |
| 공통 구현 | 제공 가능 | 가능하지만 주로 계약 표현에 사용 |
| 기존 class 적용 | 상속 또는 virtual registration 필요 | 구조가 맞으면 명시적 상속 불필요 |
framework가 lifecycle과 공통 구현을 소유하고 runtime에 잘못된 subclass 생성을 막아야 한다면 ABC가 잘 맞는다. 반대로 호출자가 필요한 method shape만 표현하고 구현체와의 결합을 낮추고 싶다면 Protocol이나 duck typing이 더 자연스럽다. 상속·Protocol·composition의 선택 기준은 Python 상속과 Interface 설계에서 이어서 다룬다.
자주 묻는 질문
ABC를 상속하면 객체를 만들 수 없나
아니다. 구현되지 않은 abstract method가 남아 있을 때만 인스턴스 생성이 차단된다. abstract method가 없는 ABC subclass는 만들 수 있다.
abstractmethod에는 반드시 pass만 써야 하나
아니다. 구현을 둘 수 있으며 subclass가 super()로 호출할 수도 있다. 다만 decorator가 붙어 있는 한 subclass는 해당 method를 override해야 concrete class가 된다.
ABC가 type hint를 대신하나
완전히 대신하지 않는다. ABC는 runtime inheritance와 인스턴스화 규칙을 제공하고, type annotation과 static checker는 별도의 검사를 맡는다. 필요에 따라 함께 사용한다.
참고 자료
'배움과 성장 > 소프트웨어 개발' 카테고리의 다른 글
| SwiftUI 상태 관리 기준: @State·@Binding·@Observable·@StateObject 구분하기 (2) | 2024.10.20 |
|---|---|
| SwiftUI에서 View·Model·Service 나누는 기준: ViewModel은 언제 필요할까 (0) | 2024.10.17 |
| Python 상속과 Interface 설계: super·MRO·Protocol·Composition 선택 기준 (1) | 2024.09.08 |
| Python으로 다시 보는 SOLID: 다섯 원칙보다 변경 경계가 먼저다 (0) | 2024.09.08 |
| Assertion은 운영에서 꺼야 할까: Java·Python·TypeScript·Go의 실제 차이 (0) | 2024.08.28 |
댓글