타입 검사 및 인터페이스

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 의 타입 검사 (type checking) 와 인터페이스에 대한 이해를 높이는 방법을 배우게 됩니다. 테이블 형식 모듈을 확장하여 추상 기본 클래스 (abstract base classes) 및 인터페이스 유효성 검사 (interface validation) 와 같은 개념을 구현하여 더욱 강력하고 유지 관리 가능한 코드를 만들 것입니다.

이 랩은 이전 연습의 개념을 기반으로 하며, 타입 안전성 (type safety) 과 인터페이스 디자인 패턴에 중점을 둡니다. 목표는 함수 매개변수에 대한 타입 검사 구현, 추상 기본 클래스를 사용한 인터페이스 생성 및 사용, 그리고 코드 중복을 줄이기 위한 템플릿 메서드 패턴 (template method pattern) 적용을 포함합니다. 데이터 테이블 형식 지정을 위한 모듈인 tableformat.py와 CSV 파일을 읽기 위한 모듈인 reader.py를 수정하게 됩니다.

이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 초급 레벨의 실험이며 완료율은 92%입니다.학습자들로부터 90%의 긍정적인 리뷰율을 받았습니다.

이 단계에서는 tableformat.py 파일의 print_table() 함수를 개선할 것입니다. formatter 매개변수가 유효한 TableFormatter 인스턴스인지 확인하는 검사를 추가할 것입니다. 왜 이런 검사가 필요할까요? 타입 검사는 코드의 안전망과 같습니다. 작업 중인 데이터가 올바른 타입인지 확인하여 찾기 어려운 많은 버그를 방지하는 데 도움이 됩니다.

Python 에서 타입 검사 이해하기

타입 검사는 프로그래밍에서 매우 유용한 기술입니다. 개발 프로세스 초기에 오류를 잡아낼 수 있습니다. Python 에서는 다양한 유형의 객체를 자주 다루며, 때로는 특정 유형의 객체가 함수에 전달되기를 기대합니다. 객체가 특정 타입이거나 해당 타입의 서브클래스인지 확인하기 위해 isinstance() 함수를 사용할 수 있습니다. 예를 들어, 리스트를 예상하는 함수가 있는 경우 isinstance()를 사용하여 입력이 실제로 리스트인지 확인할 수 있습니다.

먼저, 코드 편집기에서 tableformat.py 파일을 엽니다. 파일 하단으로 스크롤하면 print_table() 함수를 찾을 수 있습니다. 초기 모습은 다음과 같습니다.

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

이 함수는 일부 데이터, 열 목록, 그리고 포맷터를 입력으로 받습니다. 그런 다음 포맷터를 사용하여 테이블을 인쇄합니다. 하지만 현재는 포맷터가 올바른 타입인지 확인하지 않습니다.

타입 검사를 추가하도록 수정해 보겠습니다. isinstance() 함수를 사용하여 formatter 매개변수가 TableFormatter의 인스턴스인지 확인합니다. 그렇지 않은 경우 명확한 메시지와 함께 TypeError를 발생시킵니다. 수정된 코드는 다음과 같습니다.

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    if not isinstance(formatter, TableFormatter):
        raise TypeError("Expected a TableFormatter")

    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

타입 검사 구현 테스트하기

이제 타입 검사를 추가했으므로 제대로 작동하는지 확인해야 합니다. test_tableformat.py라는 새 Python 파일을 만들어 보겠습니다. 여기에 넣어야 할 코드는 다음과 같습니다.

import stock
import reader
import tableformat

## Read portfolio data
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)

## Define a formatter that doesn't inherit from TableFormatter
class MyFormatter:
    def headings(self, headers):
        pass
    def row(self, rowdata):
        pass

## Try to use the non-compliant formatter
try:
    tableformat.print_table(portfolio, ['name', 'shares', 'price'], MyFormatter())
    print("Test failed - type checking not implemented")
except TypeError as e:
    print(f"Test passed - caught error: {e}")

이 코드에서는 먼저 일부 포트폴리오 데이터를 읽습니다. 그런 다음 TableFormatter를 상속하지 않는 MyFormatter라는 새 포맷터 클래스를 정의합니다. print_table() 함수에서 이 비호환 포맷터를 사용해 봅니다. 타입 검사가 작동하면 TypeError가 발생해야 합니다.

테스트를 실행하려면 터미널을 열고 test_tableformat.py 파일이 있는 디렉토리로 이동합니다. 그런 다음 다음 명령을 실행합니다.

python test_tableformat.py

모든 것이 제대로 작동하면 다음과 같은 출력이 표시됩니다.

Test passed - caught error: Expected a TableFormatter

이 출력은 타입 검사가 예상대로 작동함을 확인합니다. 이제 print_table() 함수는 TableFormatter의 인스턴스 또는 해당 서브클래스 중 하나인 포맷터만 허용합니다.

추상 기본 클래스 (Abstract Base Class) 구현하기

이 단계에서는 Python 의 abc 모듈을 사용하여 TableFormatter 클래스를 적절한 추상 기본 클래스 (ABC) 로 변환할 것입니다. 하지만 먼저 추상 기본 클래스가 무엇이며 왜 필요한지 이해해 보겠습니다.

추상 기본 클래스 이해하기

추상 기본 클래스는 Python 의 특수한 유형의 클래스입니다. 직접 객체를 생성할 수 없는 클래스이므로 인스턴스화할 수 없습니다. 추상 기본 클래스의 주요 목적은 서브클래스에 대한 공통 인터페이스를 정의하는 것입니다. 모든 서브클래스가 따라야 하는 규칙 집합을 설정합니다. 특히, 서브클래스가 특정 메서드를 구현하도록 요구합니다.

추상 기본 클래스에 대한 몇 가지 주요 개념은 다음과 같습니다.

  • Python 에서 추상 기본 클래스를 생성하기 위해 abc 모듈을 사용합니다.
  • @abstractmethod 데코레이터로 표시된 메서드는 규칙과 같습니다. 추상 기본 클래스에서 상속받는 모든 서브클래스는 이러한 메서드를 구현해야 합니다.
  • 추상 기본 클래스에서 상속받았지만 필요한 모든 메서드를 구현하지 않은 클래스의 객체를 생성하려고 하면 Python 에서 오류가 발생합니다.

이제 추상 기본 클래스의 기본 사항을 이해했으므로 TableFormatter 클래스를 수정하여 추상 기본 클래스가 되는 방법을 살펴보겠습니다.

TableFormatter 클래스 수정하기

tableformat.py 파일을 엽니다. TableFormatter 클래스를 변경하여 abc 모듈을 사용하고 추상 기본 클래스가 되도록 할 것입니다.

  1. 먼저, abc 모듈에서 필요한 것을 가져와야 합니다. 파일 상단에 다음 import 문을 추가합니다.
## tableformat.py
from abc import ABC, abstractmethod

이 import 문은 두 가지 중요한 사항을 가져옵니다. ABC는 Python 의 모든 추상 기본 클래스의 기본 클래스이고, abstractmethod는 메서드를 추상으로 표시하는 데 사용할 데코레이터입니다.

  1. 다음으로, TableFormatter 클래스를 수정합니다. 추상 기본 클래스가 되려면 ABC에서 상속받아야 하며, @abstractmethod 데코레이터를 사용하여 메서드를 추상으로 표시합니다. 수정된 클래스는 다음과 같아야 합니다.
class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        '''
        Emit the table headings.
        '''
        pass

    @abstractmethod
    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        pass

이 수정된 클래스에 대해 몇 가지 사항에 유의하십시오.

  • 클래스는 이제 ABC에서 상속받으므로 공식적으로 추상 기본 클래스입니다.
  • headingsrow 메서드는 모두 @abstractmethod로 데코레이트됩니다. 이는 TableFormatter의 모든 서브클래스가 이러한 메서드를 구현해야 함을 Python 에 알려줍니다.
  • NotImplementedErrorpass로 대체했습니다. @abstractmethod 데코레이터는 서브클래스가 이러한 메서드를 구현하도록 하는 역할을 하므로 더 이상 NotImplementedError가 필요하지 않습니다.

추상 기본 클래스 테스트하기

이제 TableFormatter 클래스를 추상 기본 클래스로 만들었으므로 제대로 작동하는지 테스트해 보겠습니다. 다음 코드를 사용하여 test_abc.py라는 파일을 만들 것입니다.

from tableformat import TableFormatter

## Test case 1: Define a class with a misspelled method
try:
    class NewFormatter(TableFormatter):
        def headers(self, headings):  ## Misspelled 'headings'
            pass
        def row(self, rowdata):
            pass

    f = NewFormatter()
    print("Test 1 failed - abstract method enforcement not working")
except TypeError as e:
    print(f"Test 1 passed - caught error: {e}")

## Test case 2: Define a class that properly implements all methods
try:
    class ProperFormatter(TableFormatter):
        def headings(self, headers):
            pass
        def row(self, rowdata):
            pass

    f = ProperFormatter()
    print("Test 2 passed - proper implementation works")
except TypeError as e:
    print(f"Test 2 failed - error: {e}")

이 코드에는 두 가지 테스트 케이스가 있습니다. 첫 번째 테스트 케이스는 TableFormatter에서 상속받으려고 하지만 메서드 이름이 잘못된 NewFormatter 클래스를 정의합니다. 두 번째 테스트 케이스는 필요한 모든 메서드를 올바르게 구현하는 ProperFormatter 클래스를 정의합니다.

테스트를 실행하려면 터미널을 열고 다음 명령을 실행합니다.

python test_abc.py

다음과 유사한 출력이 표시됩니다.

Test 1 passed - caught error: Can't instantiate abstract class NewFormatter with abstract methods headings
Test 2 passed - proper implementation works

이 출력은 추상 기본 클래스가 예상대로 작동함을 확인합니다. 첫 번째 테스트 케이스는 NewFormatter 클래스가 headings 메서드를 올바르게 구현하지 않았기 때문에 실패합니다. 두 번째 테스트 케이스는 ProperFormatter 클래스가 필요한 모든 메서드를 구현했기 때문에 통과합니다.

알고리즘 템플릿 클래스 생성하기

이 단계에서는 추상 기본 클래스를 사용하여 템플릿 메서드 패턴을 구현할 것입니다. 목표는 CSV 파싱 기능에서 코드 중복을 줄이는 것입니다. 코드 중복은 코드를 유지 관리하고 업데이트하기 어렵게 만들 수 있습니다. 템플릿 메서드 패턴을 사용하면 CSV 파싱 코드에 대한 공통 구조를 만들고 서브클래스가 특정 세부 사항을 처리하도록 할 수 있습니다.

템플릿 메서드 패턴 이해하기

템플릿 메서드 패턴은 행동 디자인 패턴입니다. 알고리즘의 청사진과 같습니다. 메서드에서 알고리즘의 전체 구조 또는 "골격"을 정의합니다. 그러나 모든 단계를 완전히 구현하지는 않습니다. 대신, 일부 단계를 서브클래스에 위임합니다. 즉, 서브클래스는 전체 구조를 변경하지 않고 알고리즘의 특정 부분을 재정의할 수 있습니다.

이 경우, reader.py 파일을 살펴보면 read_csv_as_dicts()read_csv_as_instances() 함수가 많은 유사한 코드를 가지고 있음을 알 수 있습니다. 이들 간의 주요 차이점은 CSV 파일의 행에서 레코드를 생성하는 방식입니다. 템플릿 메서드 패턴을 사용하면 동일한 코드를 여러 번 작성하는 것을 피할 수 있습니다.

CSVParser 기본 클래스 추가하기

CSV 파싱을 위한 추상 기본 클래스를 추가하는 것으로 시작해 보겠습니다. reader.py 파일을 엽니다. import 문 바로 뒤, 파일 맨 위에 CSVParser 추상 기본 클래스를 추가합니다.

## reader.py
import csv
from abc import ABC, abstractmethod

class CSVParser(ABC):
    def parse(self, filename):
        records = []
        with open(filename) as f:
            rows = csv.reader(f)
            headers = next(rows)
            for row in rows:
                record = self.make_record(headers, row)
                records.append(record)
        return records

    @abstractmethod
    def make_record(self, headers, row):
        pass

CSVParser 클래스는 CSV 파싱을 위한 템플릿 역할을 합니다. parse 메서드는 파일을 열고, 헤더를 가져오고, 행을 반복하는 등 CSV 파일을 읽기 위한 공통 단계를 포함합니다. 행에서 레코드를 생성하기 위한 특정 로직은 make_record() 메서드로 추상화됩니다. 추상 메서드이므로 CSVParser에서 상속받는 모든 클래스는 이 메서드를 구현해야 합니다.

구체적인 파서 클래스 구현하기

이제 기본 클래스가 있으므로 구체적인 파서 클래스를 생성해야 합니다. 이러한 클래스는 특정 레코드 생성 로직을 구현합니다.

class DictCSVParser(CSVParser):
    def __init__(self, types):
        self.types = types

    def make_record(self, headers, row):
        return { name: func(val) for name, func, val in zip(headers, self.types, row) }

class InstanceCSVParser(CSVParser):
    def __init__(self, cls):
        self.cls = cls

    def make_record(self, headers, row):
        return self.cls.from_row(row)

DictCSVParser 클래스는 레코드를 딕셔너리로 생성하는 데 사용됩니다. 생성자에서 유형 목록을 사용합니다. make_record 메서드는 이러한 유형을 사용하여 행의 값을 변환하고 딕셔너리를 생성합니다.

InstanceCSVParser 클래스는 레코드를 클래스의 인스턴스로 생성하는 데 사용됩니다. 생성자에서 클래스를 사용합니다. make_record 메서드는 해당 클래스의 from_row 메서드를 호출하여 행에서 인스턴스를 생성합니다.

원래 함수 리팩토링하기

이제 이러한 새 클래스를 사용하도록 원래 read_csv_as_dicts()read_csv_as_instances() 함수를 리팩토링해 보겠습니다.

def read_csv_as_dicts(filename, types):
    '''
    Read a CSV file into a list of dictionaries with appropriate type conversion.
    '''
    parser = DictCSVParser(types)
    return parser.parse(filename)

def read_csv_as_instances(filename, cls):
    '''
    Read a CSV file into a list of instances of a class.
    '''
    parser = InstanceCSVParser(cls)
    return parser.parse(filename)

이러한 리팩토링된 함수는 원래 함수와 동일한 인터페이스를 갖습니다. 그러나 내부적으로는 방금 생성한 새 파서 클래스를 사용합니다. 이러한 방식으로, 공통 CSV 파싱 로직을 특정 레코드 생성 로직에서 분리했습니다.

구현 테스트하기

리팩토링된 코드가 제대로 작동하는지 확인해 보겠습니다. test_reader.py라는 파일을 만들고 다음 코드를 추가합니다.

import reader
import stock

## Test the refactored read_csv_as_instances function
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("First stock:", portfolio[0])

## Test the refactored read_csv_as_dicts function
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First stock as dict:", portfolio_dicts[0])

## Test direct use of a parser
parser = reader.DictCSVParser([str, int, float])
portfolio_dicts2 = parser.parse('portfolio.csv')
print("First stock from direct parser:", portfolio_dicts2[0])

테스트를 실행하려면 터미널을 열고 다음 명령을 실행합니다.

python test_reader.py

다음과 유사한 출력이 표시됩니다.

First stock: Stock('AA', 100, 32.2)
First stock as dict: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock from direct parser: {'name': 'AA', 'shares': 100, 'price': 32.2}

이 출력이 표시되면 리팩토링된 코드가 제대로 작동하는 것입니다. 원래 함수와 파서의 직접 사용 모두 예상 결과를 생성하고 있습니다.

요약

이 랩에서는 Python 코드를 향상시키기 위해 몇 가지 핵심 객체 지향 프로그래밍 (object-oriented programming) 개념을 배웠습니다. 먼저, print_table() 함수에서 타입 검사 (type checking) 를 구현하여 유효한 포맷터 (formatter) 만 사용하도록 보장함으로써 코드의 견고성을 향상시켰습니다. 둘째, TableFormatter 클래스를 추상 기본 클래스 (abstract base class) 로 변환하여 서브클래스가 특정 메서드를 구현하도록 했습니다.

또한, CSVParser 추상 기본 클래스와 구체적인 구현을 생성하여 템플릿 메서드 패턴 (template method pattern) 을 적용했습니다. 이는 일관된 알고리즘 구조를 유지하면서 코드 중복을 줄여줍니다. 이러한 기술은 특히 대규모 애플리케이션에서 더욱 유지 관리 가능하고 견고한 Python 코드를 생성하는 데 중요합니다. 학습을 더 진행하려면 Python 의 타입 힌트 (type hints, PEP 484), 프로토콜 클래스 (protocol classes), 그리고 Python 의 디자인 패턴 (design patterns) 을 탐구해 보십시오.