올바른 호출 가능 객체 정의

Beginner

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

소개

이 랩에서는 Python 의 호출 가능 객체 (callable object) 에 대해 배우게 됩니다. 호출 가능 객체는 object() 구문을 사용하여 함수처럼 호출할 수 있습니다. Python 함수는 본질적으로 호출 가능하지만, __call__ 메서드를 구현하여 사용자 정의 호출 가능 객체를 생성할 수 있습니다.

또한 __call__ 메서드를 사용하여 호출 가능 객체를 구현하고, 매개변수 유효성 검사를 위해 호출 가능 객체와 함께 함수 주석 (function annotations) 을 사용하는 방법을 배우게 됩니다. 이 랩에서는 validate.py 파일을 수정하게 됩니다.

Validator 클래스 이해하기

이 랩에서는 호출 가능 객체를 생성하기 위해 일련의 validator 클래스를 기반으로 구축할 것입니다. 구축을 시작하기 전에 validate.py 파일에 제공된 validator 클래스를 이해하는 것이 중요합니다. 이러한 클래스는 코드의 예상대로 작동하는지 확인하는 데 중요한 부분인 타입 검사 (type checking) 를 수행하는 데 도움이 됩니다.

WebIDE 에서 validate.py 파일을 열어 시작해 보겠습니다. 이 파일에는 우리가 사용할 validator 클래스에 대한 코드가 포함되어 있습니다. 파일을 열려면 터미널에서 다음 명령을 실행하십시오.

code /home/labex/project/validate.py

파일을 열면 여러 클래스가 포함되어 있는 것을 볼 수 있습니다. 각 클래스가 수행하는 작업에 대한 간략한 개요는 다음과 같습니다.

  1. Validator: 이것은 기본 클래스입니다. check 메서드가 있지만, 현재 이 메서드는 아무 작업도 수행하지 않습니다. 다른 validator 클래스의 시작점으로 사용됩니다.
  2. Typed: 이것은 Validator의 서브클래스입니다. 주요 역할은 값이 특정 유형인지 확인하는 것입니다.
  3. Integer, Float, 및 String: 이들은 Typed에서 상속되는 특정 타입 validator 입니다. 각각 값이 정수, 부동 소수점 또는 문자열인지 확인하도록 설계되었습니다.

이제 이러한 validator 클래스가 실제로 어떻게 작동하는지 살펴보겠습니다. 이를 테스트하기 위해 test.py라는 새 파일을 만들 것입니다. 이 파일을 생성하고 열려면 다음 명령을 실행하십시오.

code /home/labex/project/test.py

test.py 파일이 열리면 다음 코드를 추가하십시오. 이 코드는 IntegerString validator 를 테스트합니다.

from validate import Integer, String, Float

## Test Integer validator
print("Testing Integer validator:")
try:
    Integer.check(42)
    print("✓ Integer check passed for 42")
except TypeError as e:
    print(f"✗ Error: {e}")

try:
    Integer.check("Hello")
    print("✗ Integer check incorrectly passed for 'Hello'")
except TypeError as e:
    print(f"✓ Correctly raised error: {e}")

## Test String validator
print("\nTesting String validator:")
try:
    String.check("Hello")
    print("✓ String check passed for 'Hello'")
except TypeError as e:
    print(f"✗ Error: {e}")

이 코드에서는 먼저 validate.py 파일에서 Integer, String, 및 Float validator 를 가져옵니다. 그런 다음 정수 값 (42) 과 문자열 값 ("Hello") 을 확인하여 Integer validator 를 테스트합니다. 정수에 대한 검사가 통과하면 성공 메시지를 출력합니다. 문자열에 대해 잘못 통과하면 오류 메시지를 출력합니다. 문자열에 대해 검사가 TypeError를 올바르게 발생시키면 성공 메시지를 출력합니다. String validator 에 대해서도 유사한 테스트를 수행합니다.

코드를 추가한 후 다음 명령을 사용하여 테스트 파일을 실행하십시오.

python3 /home/labex/project/test.py

다음과 유사한 출력을 볼 수 있습니다.

Testing Integer validator:
✓ Integer check passed for 42
✓ Correctly raised error: Expected <class 'int'>

Testing String validator:
✓ String check passed for 'Hello'

보시다시피, 이러한 validator 클래스를 사용하면 타입 검사를 쉽게 수행할 수 있습니다. 예를 들어, Integer.check(x)를 호출하면 x가 정수가 아닌 경우 TypeError가 발생합니다.

이제 실제 시나리오를 생각해 보겠습니다. 인수가 특정 유형이어야 하는 함수가 있다고 가정해 보겠습니다. 다음은 그러한 함수의 예입니다.

def add(x, y):
    Integer.check(x)  ## Make sure x is an integer
    Integer.check(y)  ## Make sure y is an integer
    return x + y

이 함수는 작동하지만 문제가 있습니다. 타입 검사를 사용하고 싶을 때마다 수동으로 validator 검사를 추가해야 합니다. 특히 더 큰 함수나 프로젝트의 경우 시간이 많이 걸리고 오류가 발생하기 쉽습니다.

다음 단계에서는 호출 가능 객체를 생성하여 이 문제를 해결할 것입니다. 이 객체는 함수 주석 (function annotations) 을 기반으로 이러한 타입 검사를 자동으로 적용할 수 있습니다. 이렇게 하면 매번 검사를 수동으로 추가할 필요가 없습니다.

기본 호출 가능 객체 생성하기

Python 에서 호출 가능 객체 (callable object) 는 함수처럼 사용할 수 있는 객체입니다. 일반 함수를 호출하는 방식과 유사하게, 괄호를 붙여서 "호출"할 수 있는 것으로 생각할 수 있습니다. Python 에서 클래스가 호출 가능 객체처럼 작동하게 하려면 __call__이라는 특수 메서드를 구현해야 합니다. 이 메서드는 함수를 호출할 때와 마찬가지로, 괄호와 함께 객체를 사용할 때 자동으로 호출됩니다.

validate.py 파일을 수정하여 시작해 보겠습니다. 이 파일에 ValidatedFunction이라는 새 클래스를 추가할 것이며, 이 클래스가 우리의 호출 가능 객체가 될 것입니다. 코드 편집기에서 파일을 열려면 터미널에서 다음 명령을 실행하십시오.

code /home/labex/project/validate.py

파일이 열리면 파일 끝으로 스크롤하여 다음 코드를 추가하십시오.

class ValidatedFunction:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Calling', self.func)
        result = self.func(*args, **kwargs)
        return result

이 코드가 무엇을 하는지 자세히 살펴보겠습니다. ValidatedFunction 클래스에는 생성자인 __init__ 메서드가 있습니다. 이 클래스의 인스턴스를 생성할 때 함수를 전달합니다. 이 함수는 self.func라는 인스턴스의 속성으로 저장됩니다.

__call__ 메서드는 이 클래스를 호출 가능하게 만드는 핵심 부분입니다. ValidatedFunction 클래스의 인스턴스를 호출하면 이 __call__ 메서드가 실행됩니다. 단계별로 살펴보면 다음과 같습니다.

  1. 호출되는 함수를 알려주는 메시지를 출력합니다. 이는 디버깅 및 진행 상황 이해에 유용합니다.
  2. 인스턴스를 호출할 때 전달한 인수를 사용하여 self.func에 저장된 함수를 호출합니다. *args**kwargs를 사용하면 임의의 수의 위치 및 키워드 인수를 전달할 수 있습니다.
  3. 함수 호출의 결과를 반환합니다.

이제 이 ValidatedFunction 클래스를 테스트해 보겠습니다. 테스트 코드를 작성하기 위해 test_callable.py라는 새 파일을 만들 것입니다. 코드 편집기에서 이 새 파일을 열려면 다음 명령을 실행하십시오.

code /home/labex/project/test_callable.py

test_callable.py 파일에 다음 코드를 추가하십시오.

from validate import ValidatedFunction

def add(x, y):
    return x + y

## Wrap the add function with ValidatedFunction
validated_add = ValidatedFunction(add)

## Call the wrapped function
result = validated_add(2, 3)
print(f"Result: {result}")

## Try another call
result = validated_add(10, 20)
print(f"Result: {result}")

이 코드에서는 먼저 validate.py 파일에서 ValidatedFunction 클래스를 가져옵니다. 그런 다음 두 개의 숫자를 받아 합계를 반환하는 add라는 간단한 함수를 정의합니다.

add 함수를 전달하여 ValidatedFunction 클래스의 인스턴스를 생성합니다. 이렇게 하면 add 함수가 ValidatedFunction 인스턴스 내부에 "래핑"됩니다.

그런 다음 래핑된 함수를 두 번 호출합니다. 한 번은 인수 23으로, 다른 한 번은 1020으로 호출합니다. 래핑된 함수를 호출할 때마다 ValidatedFunction 클래스의 __call__ 메서드가 호출되고, 이 메서드는 다시 원래의 add 함수를 호출합니다.

테스트 코드를 실행하려면 터미널에서 다음 명령을 실행하십시오.

python3 /home/labex/project/test_callable.py

다음과 유사한 출력을 볼 수 있습니다.

Calling <function add at 0x7f2d1c3a9940>
Result: 5
Calling <function add at 0x7f2d1c3a9940>
Result: 30

이 출력은 우리의 호출 가능 객체가 예상대로 작동하고 있음을 보여줍니다. validated_add(2, 3)을 호출하면 실제로 ValidatedFunction 클래스의 __call__ 메서드를 호출하고, 이 메서드는 원래의 add 함수를 호출합니다.

현재 ValidatedFunction 클래스는 메시지를 출력하고 호출을 원래 함수로 전달하기만 합니다. 다음 단계에서는 함수의 주석 (annotations) 을 기반으로 타입 유효성 검사를 수행하도록 이 클래스를 개선할 것입니다.

함수 주석을 사용한 타입 유효성 검사 구현하기

Python 에서는 함수 매개변수에 타입 주석 (type annotation) 을 추가할 수 있습니다. 이러한 주석은 매개변수와 함수의 반환 값에 대한 예상 데이터 유형을 나타내는 방법으로 사용됩니다. 기본적으로 런타임에 타입을 강제하지 않지만, 유효성 검사 목적으로 사용할 수 있습니다.

예제를 살펴보겠습니다.

def add(x: int, y: int) -> int:
    return x + y

이 코드에서 x: inty: int는 매개변수 xy가 정수여야 함을 알려줍니다. 마지막의 -> intadd 함수가 정수를 반환함을 나타냅니다. 이러한 타입 주석은 함수의 __annotations__ 속성에 저장되며, 이는 매개변수 이름을 해당 주석 처리된 타입에 매핑하는 딕셔너리입니다.

이제 이러한 타입 주석을 유효성 검사에 사용하도록 ValidatedFunction 클래스를 개선할 것입니다. 이를 위해 Python 의 inspect 모듈을 사용해야 합니다. 이 모듈은 모듈, 클래스, 메서드, 함수 등과 같은 라이브 객체에 대한 정보를 얻는 데 유용한 함수를 제공합니다. 이 경우 함수 인수를 해당 매개변수 이름과 일치시키는 데 사용할 것입니다.

먼저 validate.py 파일에서 ValidatedFunction 클래스를 수정해야 합니다. 다음 명령을 사용하여 이 파일을 열 수 있습니다.

code /home/labex/project/validate.py

기존 ValidatedFunction 클래스를 다음 개선된 버전으로 바꾸십시오.

import inspect

class ValidatedFunction:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __call__(self, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(*args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(*args, **kwargs)

이 개선된 버전이 수행하는 작업은 다음과 같습니다.

  1. inspect.signature()를 사용하여 함수의 매개변수에 대한 정보 (예: 이름, 기본값 및 주석 처리된 타입) 를 얻습니다.
  2. 시그니처의 bind() 메서드를 사용하여 제공된 인수를 해당 매개변수 이름과 일치시킵니다. 이렇게 하면 각 인수를 함수에서 해당 매개변수와 연결하는 데 도움이 됩니다.
  3. 각 인수를 해당 타입 주석 (있는 경우) 과 비교하여 확인합니다. 주석이 발견되면 주석에서 validator 클래스를 검색하고 check() 메서드를 사용하여 유효성 검사를 적용합니다.
  4. 마지막으로, 유효성 검사를 거친 인수를 사용하여 원래 함수를 호출합니다.

이제 타입 주석에서 validator 클래스를 사용하는 몇 가지 함수로 이 개선된 ValidatedFunction 클래스를 테스트해 보겠습니다. 다음 명령을 사용하여 test_validation.py 파일을 여십시오.

code /home/labex/project/test_validation.py

파일에 다음 코드를 추가하십시오.

from validate import ValidatedFunction, Integer, String

def greet(name: String, times: Integer):
    return name * times

## Wrap the greet function with ValidatedFunction
validated_greet = ValidatedFunction(greet)

## Valid call
try:
    result = validated_greet("Hello ", 3)
    print(f"Valid call result: {result}")
except TypeError as e:
    print(f"Unexpected error: {e}")

## Invalid call - wrong type for 'name'
try:
    result = validated_greet(123, 3)
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for name: {e}")

## Invalid call - wrong type for 'times'
try:
    result = validated_greet("Hello ", "3")
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for times: {e}")

이 코드에서는 name: Stringtimes: Integer의 타입 주석이 있는 greet 함수를 정의합니다. 즉, name 매개변수는 String 클래스를 사용하여 유효성 검사를 받아야 하고, times 매개변수는 Integer 클래스를 사용하여 유효성 검사를 받아야 합니다. 그런 다음 타입 유효성 검사를 활성화하기 위해 greet 함수를 ValidatedFunction 클래스로 래핑합니다.

세 가지 테스트 케이스를 수행합니다. 유효한 호출, name에 대한 잘못된 타입의 잘못된 호출, times에 대한 잘못된 타입의 잘못된 호출입니다. 각 호출은 유효성 검사 중에 발생할 수 있는 TypeError 예외를 catch 하기 위해 try-except 블록으로 래핑됩니다.

테스트 파일을 실행하려면 다음 명령을 사용하십시오.

python3 /home/labex/project/test_validation.py

다음과 유사한 출력을 볼 수 있습니다.

Valid call result: Hello Hello Hello
Expected error for name: Expected <class 'str'>
Expected error for times: Expected <class 'int'>

이 출력은 ValidatedFunction 호출 가능 객체가 이제 함수 주석을 기반으로 타입 유효성 검사를 적용하고 있음을 보여줍니다. 잘못된 타입의 인수를 전달하면 validator 클래스가 오류를 감지하고 TypeError를 발생시킵니다. 이러한 방식으로 함수가 올바른 데이터 타입으로 호출되도록 하여 버그를 방지하고 코드를 더욱 강력하게 만들 수 있습니다.

챌린지: 호출 가능 객체를 메서드로 사용하기

Python 에서 클래스 내에서 호출 가능 객체 (callable object) 를 메서드로 사용할 때 해결해야 할 고유한 과제가 있습니다. 호출 가능 객체는 함수 자체 또는 __call__ 메서드가 있는 객체와 같이 함수처럼 "호출"할 수 있는 것입니다. 클래스 메서드로 사용될 때 Python 이 인스턴스 (self) 를 첫 번째 인수로 전달하는 방식 때문에 예상대로 작동하지 않는 경우가 있습니다.

Stock 클래스를 생성하여 이 문제를 살펴보겠습니다. 이 클래스는 이름, 주식 수, 가격과 같은 속성을 가진 주식을 나타냅니다. 또한 작업 중인 데이터가 올바른지 확인하기 위해 validator 를 사용합니다.

먼저 stock.py 파일을 열어 Stock 클래스 작성을 시작합니다. 편집기에서 파일을 열려면 다음 명령을 사용할 수 있습니다.

code /home/labex/project/stock.py

이제 다음 코드를 stock.py 파일에 추가합니다. 이 코드는 주식의 속성을 초기화하는 __init__ 메서드, 총 비용을 계산하는 cost 속성, 주식 수를 줄이는 sell 메서드를 사용하여 Stock 클래스를 정의합니다. 또한 sell 메서드에 대한 입력을 검증하기 위해 ValidatedFunction을 사용해 보겠습니다.

from validate import ValidatedFunction, Integer

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: Integer):
        self.shares -= nshares

    ## Try to use ValidatedFunction
    sell = ValidatedFunction(sell)

Stock 클래스를 정의한 후에는 예상대로 작동하는지 테스트해야 합니다. test_stock.py라는 테스트 파일을 만들고 다음 명령을 사용하여 엽니다.

code /home/labex/project/test_stock.py

다음 코드를 test_stock.py 파일에 추가합니다. 이 코드는 Stock 클래스의 인스턴스를 생성하고, 초기 주식 수와 비용을 출력하고, 일부 주식을 판매하려고 시도한 다음 업데이트된 주식 수와 비용을 출력합니다.

from stock import Stock

try:
    ## Create a stock
    s = Stock('GOOG', 100, 490.1)

    ## Get the initial cost
    print(f"Initial shares: {s.shares}")
    print(f"Initial cost: ${s.cost}")

    ## Try to sell some shares
    s.sell(10)

    ## Check the updated cost
    print(f"After selling, shares: {s.shares}")
    print(f"After selling, cost: ${s.cost}")

except Exception as e:
    print(f"Error: {e}")

이제 다음 명령을 사용하여 테스트 파일을 실행합니다.

python3 /home/labex/project/test_stock.py

다음과 유사한 오류가 발생할 것입니다.

Error: missing a required argument: 'nshares'

이 오류는 Python 이 s.sell(10)과 같은 메서드를 호출할 때 실제로 내부적으로 Stock.sell(s, 10)을 호출하기 때문에 발생합니다. self 매개변수는 클래스의 인스턴스를 나타내며 자동으로 첫 번째 인수로 전달됩니다. 그러나 ValidatedFunction은 메서드로 사용되고 있음을 알지 못하므로 이 self 매개변수를 올바르게 처리하지 않습니다.

문제 이해

클래스 내에서 메서드를 정의한 다음 ValidatedFunction으로 바꾸면 본질적으로 원래 메서드를 래핑하는 것입니다. 문제는 래핑된 메서드가 self 매개변수를 자동으로 올바르게 처리하지 않는다는 것입니다. 인스턴스가 첫 번째 인수로 전달되는 것을 고려하지 않는 방식으로 인수를 예상합니다.

문제 해결

이 문제를 해결하려면 메서드를 처리하는 방식을 수정해야 합니다. 메서드 호출을 제대로 처리할 수 있는 ValidatedMethod라는 새 클래스를 만들 것입니다. 다음 코드를 validate.py 파일의 끝에 추가합니다.

import inspect

class ValidatedMethod:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __get__(self, instance, owner):
        ## This method is called when the attribute is accessed as a method
        if instance is None:
            return self

        ## Return a callable that binds 'self' to the instance
        def method_wrapper(*args, **kwargs):
            return self(instance, *args, **kwargs)

        return method_wrapper

    def __call__(self, instance, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(instance, *args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(instance, *args, **kwargs)

이제 ValidatedFunction 대신 ValidatedMethod를 사용하도록 Stock 클래스를 수정해야 합니다. stock.py 파일을 다시 엽니다.

code /home/labex/project/stock.py

다음과 같이 Stock 클래스를 업데이트합니다.

from validate import ValidatedMethod, Integer

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @ValidatedMethod
    def sell(self, nshares: Integer):
        self.shares -= nshares

ValidatedMethod 클래스는 속성이 액세스되는 방식을 변경할 수 있는 Python 의 특수한 유형의 객체인 descriptor 입니다. __get__ 메서드는 속성이 메서드로 액세스될 때 호출됩니다. 인스턴스를 첫 번째 인수로 올바르게 전달하는 호출 가능 객체를 반환합니다.

다음 명령을 사용하여 테스트 파일을 다시 실행합니다.

python3 /home/labex/project/test_stock.py

이제 다음과 유사한 출력을 볼 수 있습니다.

Initial shares: 100
Initial cost: $49010.0
After selling, shares: 90
After selling, cost: $44109.0

이 챌린지는 호출 가능 객체의 중요한 측면을 보여주었습니다. 클래스에서 메서드로 사용할 때는 특별한 처리가 필요합니다. __get__ 메서드를 사용하여 descriptor 프로토콜을 구현함으로써 독립 실행형 함수와 메서드 모두에서 올바르게 작동하는 호출 가능 객체를 만들 수 있습니다.

요약

이 Lab 에서 Python 에서 적절한 호출 가능 객체를 만드는 방법을 배웠습니다. 먼저, 타입 검사를 위한 기본 validator 클래스를 살펴보고 __call__ 메서드를 사용하여 호출 가능 객체를 만들었습니다. 그런 다음, 함수 주석을 기반으로 유효성 검사를 수행하도록 이 객체를 개선하고 호출 가능 객체를 클래스 메서드로 사용하는 문제에 대처했습니다.

다룬 주요 개념에는 호출 가능 객체와 __call__ 메서드, 타입 힌트를 위한 함수 주석, 함수 시그니처를 검사하기 위한 inspect 모듈 사용, 클래스 메서드를 위한 __get__ 메서드를 사용한 descriptor 프로토콜이 포함됩니다. 이러한 기술을 통해 데코레이터 및 기타 고급 Python 기능에 대한 기본 패턴인 사전 및 사후 호출 처리를 위한 강력한 함수 래퍼를 만들 수 있습니다.