deep-dive2024-04-28·8 min·310/348

Python ABC (Abstract Base Class) 활용법

Python의 추상 베이스 클래스(ABC)를 활용하여 인터페이스를 정의하고 강제하는 방법을 알아봅니다.

Python ABC (Abstract Base Class) 활용법

Python의 Abstract Base Class (ABC)는 인터페이스를 정의하고, 구현 클래스가 필수 메서드를 반드시 구현하도록 강제하는 데 사용됩니다.

Environment

$ python --version
Python 3.11.5

$ python -c "from abc import ABC, abstractmethod; print('ABC module available')"
ABC module available

Problem: 인터페이스 강제가 안 됨

class Animal:
    def speak(self):
        raise NotImplementedError("Subclasses must implement speak()")

class Dog(Animal):
    pass  # Forgot to implement speak()

# No error until runtime!
dog = Dog()
dog.speak()  # NotImplementedError at runtime

Analysis: ABC 동작 원리

from abc import ABC, abstractmethod

class AbstractAnimal(ABC):
    @abstractmethod
    def speak(self):
        """Subclasses MUST implement this"""
        pass
    
    @abstractmethod
    def move(self):
        pass
    
    # Non-abstract method (optional to override)
    def sleep(self):
        return "Zzz..."

# This will raise TypeError at instantiation
try:
    animal = AbstractAnimal()
except TypeError as e:
    print(f"Error: {e}")
    # Error: Can't instantiate abstract class AbstractAnimal 
    # with abstract methods move, speak

Solution: ABC 활용법

기본 ABC 정의

from abc import ABC, abstractmethod
from typing import List

class Vehicle(ABC):
    def __init__(self, make: str, model: str, year: int):
        self.make = make
        self.model = model
        self.year = year
    
    @abstractmethod
    def start(self) -> str:
        """Start the vehicle"""
        pass
    
    @abstractmethod
    def stop(self) -> str:
        """Stop the vehicle"""
        pass
    
    @abstractmethod
    def fuel_type(self) -> str:
        """Return fuel type"""
        pass
    
    # Concrete method (can be overridden)
    def info(self) -> str:
        return f"{self.year} {self.make} {self.model}"

class Car(Vehicle):
    def start(self) -> str:
        return "Car started"
    
    def stop(self) -> str:
        return "Car stopped"
    
    def fuel_type(self) -> str:
        return "Gasoline"
    
    def drive(self) -> str:  # Additional method
        return "Driving on road"

class ElectricCar(Vehicle):
    def start(self) -> str:
        return "Electric car started silently"
    
    def stop(self) -> str:
        return "Electric car stopped"
    
    def fuel_type(self) -> str:
        return "Electricity"
    
    def charge(self) -> str:
        return "Charging battery"

# Usage
car = Car("Toyota", "Camry", 2024)
ev = ElectricCar("Tesla", "Model 3", 2024)

print(car.info())       # 2024 Toyota Camry
print(car.start())      # Car started
print(car.fuel_type())  # Gasoline

print(ev.info())        # 2024 Tesla Model 3
print(ev.charge())      # Charging battery

ABC 등록 (Register)

from abc import ABC, abstractmethod

class Serializer(ABC):
    @abstractmethod
    def serialize(self, data) -> str:
        pass

class JSONSerializer(Serializer):
    def serialize(self, data) -> str:
        import json
        return json.dumps(data)

class XMLSerializer(Serializer):
    def serialize(self, data) -> str:
        return f"{data}"

# Register without inheriting
class CSVSerializer:
    def serialize(self, data) -> str:
        return ",".join(str(v) for v in data.values())

# Register CSVSerializer as Serializer
Serializer.register(CSVSerializer)

# Check if class is registered
print(issubclass(CSVSerializer, Serializer))  # True
print(isinstance(CSVSerializer(), Serializer))  # True

ABC와 Type Hints

from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable

# Using ABC
class Drawable(ABC):
    @abstractmethod
    def draw(self) -> None:
        pass

# Using Protocol (structural subtyping)
@runtime_checkable
class Serializable(Protocol):
    def serialize(self) -> str:
        ...

class MyData:
    def serialize(self) -> str:
        return "serialized data"

# Check structural compatibility
data = MyData()
print(isinstance(data, Serializable))  # True

# ABC vs Protocol
# - ABC: Explicit inheritance required
# - Protocol: Structural subtyping (duck typing with type hints)

Virtual Subclass

from abc import ABC, abstractmethod

class Plugin(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Plugin:
            if any("execute" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

class MyPlugin:
    def execute(self):
        return "Executing plugin"

# MyPlugin is recognized as Plugin subclass without inheritance
print(issubclass(MyPlugin, Plugin))  # True
print(isinstance(MyPlugin(), Plugin))  # True

Advanced: 복잡한 ABC 패턴

from abc import ABC, abstractmethod
from typing import Generic, TypeVar

T = TypeVar('T')

class Repository(ABC, Generic[T]):
    @abstractmethod
    def get(self, id: int) -> T:
        pass
    
    @abstractmethod
    def save(self, entity: T) -> None:
        pass
    
    @abstractmethod
    def delete(self, id: int) -> None:
        pass
    
    def find_all(self) -> list[T]:
        """Concrete method using abstract methods"""
        return []  # Default implementation

@dataclass
class User:
    id: int
    name: str

class UserRepository(Repository[User]):
    def __init__(self):
        self._users: dict[int, User] = {}
    
    def get(self, id: int) -> User:
        return self._users.get(id)
    
    def save(self, entity: User) -> None:
        self._users[entity.id] = entity
    
    def delete(self, id: int) -> None:
        del self._users[id]

# Usage
repo = UserRepository()
repo.save(User(1, "Joel"))
user = repo.get(1)
print(user)  # User(id=1, name='Joel')

Lessons Learned

  1. @abstractmethod 필수: ABC에서 추상 메서드로 정의하면, 하위 클래스에서 반드시 구현해야 합니다.

  2. 인스턴스 생성 방지: 추상 클래스는 직접 인스턴스를 생성할 수 없으며, 오직 상속된 클래스만 인스턴스를 만들 수 있습니다.

  3. 구체 메서드 포함: ABC에는 추상 메서드 외에 구체 메서드도 포함할 수 있습니다.

  4. register 메서드: 기존 클래스를 ABC의 서브클래스로 등록하여 타입 체크를 할 수 있습니다.

  5. Protocol과 비교: ABC는 명시적 상속이 필요하지만, Protocol은 구조적 서브타이핑(덕 타이핑)을 지원합니다.


This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.