Python 데코레이터 이해하기

PythonBeginner
지금 연습하기

소개

본 랩에서는 Python 의 데코레이터 (decorator) 에 대한 포괄적인 이해를 얻게 될 것입니다. 데코레이터는 함수나 메서드를 수정하거나 향상시키는 강력한 기능입니다. 먼저 데코레이터의 기본 개념을 소개하고 실용적인 예제를 통해 기본적인 사용법을 살펴보겠습니다.

이러한 기초를 바탕으로, 장식된 함수의 중요한 메타데이터를 보존하기 위해 functools.wraps를 효과적으로 사용하는 방법을 배웁니다. 그런 다음 property 데코레이터와 같은 특정 데코레이터를 자세히 살펴보고 속성 접근을 관리하는 역할을 이해할 것입니다. 마지막으로, 이 랩에서는 인스턴스 메서드, 클래스 메서드 및 정적 메서드 간의 차이점을 명확히 하고, 클래스 내에서 메서드 동작을 제어하기 위해 이러한 맥락에서 데코레이터가 어떻게 사용되는지 시연할 것입니다.

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

기본 데코레이터 이해하기

이 단계에서는 데코레이터의 개념과 기본적인 사용법을 소개합니다. 데코레이터는 다른 함수를 인수로 받아 일부 기능을 추가하고 또 다른 함수를 반환하는 함수이며, 이 모든 과정은 원본 함수의 소스 코드를 변경하지 않고 수행됩니다.

먼저, WebIDE 왼쪽 파일 탐색기에서 decorator_basics.py 파일을 찾으십시오. 두 번 클릭하여 엽니다. 이 파일에 첫 번째 데코레이터를 작성할 것입니다.

다음 코드를 decorator_basics.py에 복사하여 붙여넣으십시오.

import datetime

def log_activity(func):
    """A simple decorator to log function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

이 코드를 분석해 보겠습니다.

  • 우리는 함수 func를 인수로 받아들이는 데코레이터 함수 log_activity를 정의합니다.
  • log_activity 내부에서 중첩 함수 wrapper를 정의합니다. 이 함수는 새로운 동작을 포함하게 됩니다. 로그 메시지를 출력하고, 원본 함수 func를 호출한 다음, 또 다른 로그 메시지를 출력합니다.
  • log_activity 함수는 wrapper 함수를 반환합니다.
  • greet 함수 위의 @log_activity 구문은 greet = log_activity(greet)의 단축 표현입니다. 이는 우리의 데코레이터를 greet 함수에 적용합니다.

이제 파일을 저장합니다 ( Ctrl+S 또는 Cmd+S를 사용할 수 있습니다). 스크립트를 실행하려면 WebIDE 하단에 있는 통합 터미널을 열고 다음 명령을 실행하십시오.

python ~/project/decorator_basics.py

다음과 같은 출력을 보게 될 것입니다. 날짜와 시간은 다를 수 있습니다.

Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.

Function name: wrapper
Function docstring: None

출력에서 두 가지 사항에 주목하십시오. 첫째, 우리의 greet 함수는 이제 로깅 메시지로 래핑 (wrapped) 되었습니다. 둘째, 함수의 이름과 docstring 이 wrapper 함수의 이름과 docstring 으로 대체되었습니다. 이는 디버깅 및 인트로스펙션 (introspection) 에 문제가 될 수 있습니다. 다음 단계에서는 이를 수정하는 방법을 배울 것입니다.

functools.wraps 를 사용한 함수 메타데이터 보존

이전 단계에서 우리는 함수를 장식 (decorate) 하면 원래의 메타데이터 (__name__, __doc__ 등) 가 래퍼 (wrapper) 함수의 메타데이터로 대체된다는 것을 확인했습니다. Python 의 functools 모듈은 이에 대한 해결책을 제공합니다. 바로 wraps 데코레이터입니다.

wraps 데코레이터는 사용자 정의 데코레이터 내부에서 사용하여 원본 함수의 메타데이터를 래퍼 함수로 복사합니다.

decorator_basics.py 파일의 코드를 수정해 보겠습니다. WebIDE 에서 파일을 열고 functools.wraps를 사용하도록 업데이트하십시오.

import datetime
from functools import wraps

def log_activity(func):
    """A simple decorator to log function calls."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

변경된 사항은 다음과 같습니다.

  1. functools 모듈에서 wraps를 가져왔습니다.
  2. wrapper 함수 정의 바로 위에 @wraps(func)를 추가했습니다.

파일을 저장하고 터미널에서 다시 실행하십시오.

python ~/project/decorator_basics.py

이제 출력 결과가 달라질 것입니다.

Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.

Function name: greet
Function docstring: A simple function to greet someone.

보시다시피, 함수 이름은 올바르게 greet로 보고되며 원래의 docstring 도 보존됩니다. functools.wraps를 사용하는 것은 데코레이터를 더 강력하고 전문적으로 만드는 모범 사례입니다.

@property 를 사용한 관리 속성 구현

Python 은 여러 내장 데코레이터를 제공합니다. 그중 가장 유용한 것 중 하나는 @property로, 클래스 메서드를 "관리형 속성 (managed attribute)"으로 변환할 수 있게 해줍니다. 이는 사용자가 클래스와 상호 작용하는 방식을 변경하지 않으면서 속성 접근에 유효성 검사나 계산과 같은 로직을 추가하는 데 이상적입니다.

Circle 클래스를 생성하여 이를 살펴보겠습니다. 파일 탐색기에서 property_decorator.py 파일을 여십시오.

다음 코드를 property_decorator.py에 복사하여 붙여넣으십시오.

import math

class Circle:
    def __init__(self, radius):
        ## The actual value is stored in a "private" attribute
        self._radius = radius

    @property
    def radius(self):
        """The radius property."""
        print("Getting radius...")
        return self._radius

    @radius.setter
    def radius(self, value):
        """The radius setter with validation."""
        print(f"Setting radius to {value}...")
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """A read-only computed property for the area."""
        print("Calculating area...")
        return math.pi * self._radius ** 2

## --- Let's test our Circle class ---
c = Circle(5)

## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")

## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")

## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")

## Try to set an invalid radius (triggers the setter's validation)
try:
    c.radius = -2
except ValueError as e:
    print(f"Error: {e}")

이 코드에서:

  • radius 메서드 위의 @property는 "getter"를 정의합니다. c.radius에 접근할 때 호출됩니다.
  • @radius.setterradius 속성에 대한 "setter"를 정의합니다. c.radius = 10과 같이 값을 할당할 때 호출됩니다. 여기서는 음수 값을 방지하기 위해 유효성 검사를 추가했습니다.
  • area 메서드도 @property를 사용하지만 setter 가 없으므로 읽기 전용 속성이 됩니다. 이 값은 접근할 때마다 계산됩니다.

파일을 저장하고 터미널에서 실행하십시오.

python ~/project/property_decorator.py

다음과 같은 출력을 보게 될 것이며, getter, setter 및 유효성 검사 로직이 자동으로 호출되는 방식을 보여줍니다.

Getting radius...
Initial radius: 5

Setting radius to 10...
Getting radius...
New radius: 10

Calculating area...
Circle area: 314.16

Setting radius to -2...
Error: Radius cannot be negative

인스턴스, 클래스, 정적 메서드 구분하기

Python 클래스에서 메서드는 인스턴스, 클래스에 바인딩되거나 아예 바인딩되지 않을 수 있습니다. 이러한 다른 메서드 유형을 정의하기 위해 데코레이터가 사용됩니다.

  • 인스턴스 메서드 (Instance Methods): 기본 유형입니다. 첫 번째 인수로 인스턴스 자체를 받으며, 관례적으로 self라고 명명됩니다. 인스턴스별 데이터에 대해 작동합니다.
  • 클래스 메서드 (Class Methods): @classmethod로 표시됩니다. 첫 번째 인수로 클래스 자체를 받으며, 관례적으로 cls라고 명명됩니다. 클래스 수준 데이터에 대해 작동하며 종종 대체 생성자 (alternative constructors) 로 사용됩니다.
  • 정적 메서드 (Static Methods): @staticmethod로 표시됩니다. 특별한 첫 번째 인수를 받지 않습니다. 본질적으로 클래스 내에 네임스페이스화된 일반 함수이며 인스턴스 또는 클래스 상태에 접근할 수 없습니다.

이 세 가지 유형이 모두 작동하는 것을 살펴보겠습니다. 파일 탐색기에서 class_methods.py 파일을 여십시오.

다음 코드를 class_methods.py에 복사하여 붙여넣으십시오.

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    ## 1. Instance Method
    def instance_method(self):
        print("\n--- Calling Instance Method ---")
        print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
        print(f"Can access class data: self.class_variable = '{self.class_variable}'")

    ## 2. Class Method
    @classmethod
    def class_method(cls):
        print("\n--- Calling Class Method ---")
        print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
        ## Note: Cannot access instance_variable without an instance
        print("Cannot access instance data directly.")

    ## 3. Static Method
    @staticmethod
    def static_method(a, b):
        print("\n--- Calling Static Method ---")
        print("Cannot access instance or class data directly.")
        print(f"Just a utility function: {a} + {b} = {a + b}")

## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")

## Call the instance method (requires an instance)
my_instance.instance_method()

## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works

## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works

파일을 저장하고 터미널에서 실행하십시오.

python ~/project/class_methods.py

출력을 주의 깊게 검토하십시오. 각 메서드 유형의 기능과 한계를 명확하게 보여줍니다.

--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28

이 예제는 인스턴스 상태, 클래스 상태 또는 둘 다 필요하지 않은지에 따라 각 메서드 유형을 사용해야 하는 경우에 대한 명확한 참조를 제공합니다.

요약

본 실습 (lab) 을 통해 Python 의 데코레이터에 대한 실질적인 이해를 얻었습니다. 먼저 기본 데코레이터를 생성하고 적용하여 함수에 기능을 추가하는 방법을 배웠습니다. 그런 다음, 깔끔하고 유지보수 가능한 데코레이터를 작성하는 데 필수적인 모범 사례인 functools.wraps를 사용하여 원본 함수의 메타데이터를 보존하는 것의 중요성을 확인했습니다.

더 나아가 강력한 내장 데코레이터들을 탐구했습니다. @property 데코레이터를 사용하여 사용자 정의 getter 및 setter 로직을 가진 관리형 속성 (managed attributes) 을 생성하는 방법을 배웠으며, 이를 통해 입력 유효성 검사와 같은 기능을 구현할 수 있었습니다. 마지막으로, 인스턴스 메서드, 클래스 메서드 (@classmethod), 정적 메서드 (@staticmethod) 를 구분하고, 각 메서드가 인스턴스 상태 및 클래스 상태에 대한 접근 권한에 따라 클래스 구조 내에서 어떻게 다른 목적을 수행하는지 이해했습니다.