본문 바로가기

Python study

함수 데커레이터(Decorator) & 클로저(Closure)

데코레이터(Decorator)

파이썬의 데커레이터는 기본적으로 함수나 메서드의 동작을 변경하거나 확장하기 위한 강력한 도구이다. 데커레이터는 다른 함수를 인자로 받는 함수이다. 데커레이터는 함수를 감싸는(또는 장식하는) 방식으로 작동하여 함수의 전후에 추가적인 코드를 실행할 수 있게 해준다.

데커레이터의 작동 원리

파이썬에서 데커레이터는 다음과 같이 작동한다:

  1. 데커레이터는 함수를 인자로 받는다.
  2. 데커레이터 내부에서, 원래의 함수를 호출하는 새로운 함수를 정의한다.
  3. 이 새로운 함수는 추가적인 기능을 수행하고, 원래의 함수를 호출할 수 있다.
  4. 데커레이터는 이 새로운 함수를 반환한다. 이 반환된 함수는 원래의 함수를 "장식"하고, 원래 함수명을 사용하여 호출될 때마다 새로운 기능을 실행한다.

기본 데커레이터 예시

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

위의 코드에서, say_hello() 함수는 my_decorator에 의해 장식되어 있다. say_hello를 호출할 때, 실제로 호출되는 것은 wrapper 함수이다. 이 wrapper 함수 내에서는 원래의 say_hello() 함수가 호출되기 전후로 추가적인 코드가 실행된다.

파라미터가 있는 함수 장식하기

함수가 인자를 받을 때, 데커레이터 내의 wrapper 함수도 이 인자를 처리할 수 있도록 해야 한다.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper

@my_decorator
def say_something(name):
    print(f"Hello {name}!")

say_something("Alice")

여기서 wrapper 함수는 모든 위치 기반 및 키워드 기반 인자를 받을 수 있게 *args와 **kwargs를 사용한다.

데커레이터의 활용

데커레이터는 로깅, 액세스 제어, 측정 및 성능 향상, 트랜잭션 처리 등 다양한 영역에서 활용될 수 있다. 또한, 여러 데커레이터를 하나의 함수에 적용할 수도 있다. 이를 통해 여러 기능을 층층이 추가할 수 있다.

데커레이터는 파이썬의 매우 강력한 기능 중 하나로, 코드를 간결하게 유지하면서도 확장성을 크게 향상시킬 수 있는 도구이다.

 

데커레이터 실행 시점

파이썬에서 데커레이터의 실행 시점은 프로그램의 다른 부분과 약간 다르게 동작하는 중요한 특징이 있다. 데커레이터는 기본적으로 정의 시점에 실행된다. 이는 함수가 실제로 호출되기 전에, 파이썬이 함수 정의를 읽는 순간 데커레이터가 적용된다는 의미이다.

이해를 돕기 위해 다음의 절차를 고려해 볼 수 있다:

  1. 소스 코드 로드: 파이썬이 소스 파일을 로드할 때, 모든 코드를 위에서 아래로 읽는다.
  2. 함수 정의 처리: 함수가 정의될 때, 파이썬은 먼저 함수의 본문을 읽고 이를 하나의 객체로 만든다.
  3. 데커레이터 적용: 함수 정의 다음에 데커레이터 표기(@decorator)가 있으면, 파이썬은 즉시 데커레이터 함수를 호출한다. 이때 데커레이터는 함수 객체를 인자로 받아, 일반적으로 이 함수를 감싸는 새로운 함수를 반환한다.
  4. 함수 객체 교체: 반환된 새 함수 객체는 원래의 함수 이름에 연결된다. 따라서, 원래 함수명으로 호출할 때마다 새로 반환된 함수가 실행된다.

예시

아래의 간단한 데커레이터 예시를 보면 실행 시점을 좀 더 명확하게 이해할 수 있다:

def my_decorator(func):
    print("Decorator is being applied.")
    def wrapper():
        print("Before the function is called.")
        func()
        print("After the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

print("Finished defining functions.")
say_hello()

실행 결과:

Decorator is being applied.
Finished defining functions.
Before the function is called.
Hello!
After the function is called.

위 예시에서 볼 수 있듯이, my_decorator는 say_hello 함수 정의 직후에 즉시 호출된다. 이는 say_hello() 함수가 실제로 실행되기 전이며, 심지어 프로그램의 실행 흐름이 say_hello() 호출에 도달하기 전에 발생한다.

이러한 특성 때문에 데커레이터는 함수의 동작을 수정하거나 확장하는 데 매우 유용하게 사용된다. 함수의 실행을 감시하거나, 사전/사후 처리를 추가하거나, 함수 호출을 인터셉트하는 등의 작업을 할 수 있다.

 

임포트 타임(import time)과 런타임(runtime)

파이썬에서 "임포트 타임(import time)"과 "런타임(runtime)"은 프로그램 실행 중 다른 시점을 지칭하는 용어이다. 이 두 시점의 주요 차이는 프로그램 코드가 어떻게 그리고 언제 실행되는가에 있다.

임포트 타임 (Import Time)

임포트 타임은 파이썬 모듈이나 패키지가 처음으로 임포트되는 시점을 의미한다. 이 시점에 파이썬 인터프리터는 해당 모듈의 코드를 위에서부터 아래로 실행하면서 모든 정의(함수, 클래스, 변수 등)를 읽고 처리한다. 임포트 타임에 실행되는 코드는 모듈이 임포트된 직후에 바로 실행되며, 이는 프로그램이 시작할 때 일반적으로 한 번만 발생한다. 예를 들어, 모듈 상단에 위치한 변수 초기화, 함수 및 클래스 정의, 데커레이터 적용 등이 임포트 타임에 실행된다.

런타임 (Runtime)

런타임은 프로그램이 사용자의 입력을 처리하고, 함수를 호출하며, 데이터를 조작하는 등의 동적인 활동을 수행하는 시점이다. 런타임은 프로그램이 실행되고 있는 동안 계속해서 발생하며, 사용자와의 상호작용, 함수의 실행, 조건문과 반복문의 평가 등이 여기에 해당한다. 런타임은 실제 프로그램 로직이 수행되는 시점이며, 임포트 타임에 로드된 모든 함수와 클래스가 사용될 수 있다.

차이점

  • 시점: 임포트 타임은 모듈이 처음 로드될 때 한 번 발생하고, 런타임은 프로그램 실행 동안 계속 발생한다.
  • 목적: 임포트 타임은 주로 환경 설정, 모듈 초기화 등을 위해 사용되고, 런타임은 실제 프로그램의 동작과 로직을 처리한다.
  • 예시: 데커레이터의 적용은 임포트 타임에 발생하고, 데커레이터에 의해 감싼 함수의 호출은 런타임에 발생한다.

이 두 시점의 이해는 모듈이 어떻게 작동하는지, 그리고 파이썬 프로그램의 성능과 구조를 최적화하는데 중요하다.

 

데커레이터로 개선한 전략패턴

전략 패턴은 객체지향 디자인 패턴 중 하나로, 알고리즘군을 정의하고 각각을 캡슐화하여 교체가 가능하게 만들어 주는 패턴이다. 이 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.

파이썬에서는 종종 전략 패턴을 데커레이터를 통해 구현한다. 이 방법은 클래스 대신 함수를 사용하여 간결하고 유연한 코드를 작성할 수 있도록 해준다.

데커레이터를 사용한 전략 패턴 예시

아래 예시는 텍스트를 다양한 형식(HTML, Markdown)으로 렌더링하는 전략 패턴을 구현한 것이다. 여기서 데커레이터는 전략 선택의 유연성을 높이는데 사용된다.

import functools

# 전략 인터페이스 역할의 데커레이터
def render_strategy(func):
    @functools.wraps(func)
    def wrapper(text):
        return func(text)
    return wrapper

# 구체적인 전략 1: HTML 렌더링
@render_strategy
def render_html(text):
    return f"<html><body>{text}</body></html>"

# 구체적인 전략 2: Markdown 렌더링
@render_strategy
def render_markdown(text):
    return f"**{text}**"

# 문맥을 정의하는 클래스
class TextRenderer:
    def __init__(self, render_func):
        self.render_func = render_func

    def render(self, text):
        return self.render_func(text)

# 사용 예
renderer = TextRenderer(render_html)
print(renderer.render("Hello World"))

renderer = TextRenderer(render_markdown)
print(renderer.render("Hello World"))

이 코드에서 render_strategy 데커레이터는 전략 함수에 일관된 인터페이스를 제공한다. TextRenderer 클래스는 렌더링 전략을 받아서 사용하는 문맥 역할을 한다. 이 방식을 사용하면, 렌더링 전략을 런타임에 유연하게 변경할 수 있다.

장점

  • 유연성: 데커레이터를 사용하면 전략을 손쉽게 교체할 수 있으며, 새로운 전략을 추가하는 것도 간단하다.
  • 간결성: 복잡한 클래스 구조 없이 함수만으로 전략 패턴을 구현할 수 있다.

단점

  • 명시성의 감소: 클래스 기반 접근 방식보다 전략의 역할이 코드 상에서 덜 명확할 수 있다.

이 예시처럼, 파이썬에서 데커레이터를 사용해 전략 패턴을 구현하는 것은 코드의 간결성과 유연성을 높이는 좋은 방법이다.

 

변수 범위 규칙

파이썬에서 변수의 범위(scope)는 변수가 접근 가능한 코드 영역을 의미한다. 이 범위는 변수가 어디에서 선언되었느냐에 따라 결정된다. 파이썬에서 변수 범위는 크게 네 가지로 구분된다:

  1. 로컬(Local) 범위: 함수 내부에서 선언된 변수는 그 함수 내부에서만 접근할 수 있다. 이러한 변수를 로컬 변수라고 하며, 함수 외부에서는 이 변수에 접근할 수 없다.
  2. 인클로징(Enclosing) 범위: 중첩된 함수 구조에서 내부 함수가 외부 함수의 변수에 접근할 수 있는 범위이다. 내부 함수에서는 외부 함수의 변수를 읽거나 수정할 수 있지만, 일반적으로 외부 함수의 변수를 새로 할당하려면 nonlocal 키워드를 사용해야 한다.
  3. 전역(Global) 범위: 함수 밖, 모듈 수준에서 선언된 변수는 전역 변수로, 모듈 내 어느 곳에서든 접근할 수 있다. 함수 내부에서 전역 변수를 수정하려면 global 키워드를 사용하여 해당 변수가 전역임을 명시해야 한다.
  4. 내장(Built-in) 범위: 파이썬의 내장 이름들(예: len, print 등)이 저장된 범위로, 이 이름들은 기본적으로 모든 모듈과 함수에서 접근할 수 있다.

변수 범위의 예제

아래 예제를 통해 각각의 범위를 좀 더 구체적으로 살펴보겠다.

x = 'global x'  # 전역 변수

def test():
    y = 'local y'  # 로컬 변수
    print(y)
    print(x)  # 전역 변수 x에 접근 가능

test()
# print(y)  # 에러 발생, y는 test 함수의 로컬 범위에 속함

def outer():
    z = 'outer z'  # 인클로징 변수

    def inner():
        nonlocal z
        z = 'inner z'  # outer의 z를 변경
        print(z)

    inner()
    print(z)  # 출력: 'inner z', 변경 사항이 반영됨

outer()

def change_global():
    global x
    x = 'changed global x'  # 전역 변수 x를 변경

change_global()
print(x)  # 출력: 'changed global x'

변수 범위 규칙 이해의 중요성

변수 범위를 이해하는 것은 파이썬 프로그래밍에서 중요한 요소 중 하나이다. 범위 규칙을 이해함으로써 다음과 같은 이점을 얻을 수 있다:

  • 코드의 가독성과 유지보수성이 향상된다.
  • 변수 충돌을 방지하여 버그 발생 가능성을 줄일 수 있다.
  • 코드의 모듈성을 높일 수 있다.

이와 같은 변수 범위 규칙을 통해 파이썬 프로그래머는 보다 효율적이고 안정적인 코드를 작성할 수 있다.

 

클로저(Closure)

클로저(Closure)는 프로그래밍 언어의 기능 중 하나로, 특정 함수가 자신이 선언될 때의 환경을 "기억"하는 함수를 말한다. 클로저는 내부 함수가 자신이 정의된 환경(lexical scope) 외부의 변수에 접근할 수 있는 경우 생성된다. 파이썬에서 클로저는 특히 중첩된 함수에서 외부 함수의 변수에 접근할 수 있을 때 유용하게 사용된다.

클로저가 생성되는 조건

파이썬에서 클로저는 다음 세 가지 조건을 만족할 때 생성된다:

  1. 중첩 함수가 존재해야 한다: 즉, 하나의 함수 안에 다른 함수가 정의되어야 한다.
  2. 내부 함수가 외부 함수의 변수를 참조해야 한다: 내부 함수가 외부 함수의 로컬 변수를 사용하고 있어야 한다.
  3. 외부 함수가 내부 함수를 반환해야 한다: 외부 함수의 반환 값이 내부 함수여야 클로저가 생성된다.

클로저의 예시

아래는 클로저의 간단한 예제이다. 이 예제에서 외부 함수 make_multiplier는 인자 x를 받고, 내부 함수 multiplier를 정의하여 반환한다. 내부 함수 multiplier는 외부 함수의 변수 x를 참조하고 있다.

def make_multiplier(x):
    def multiplier(y):
        return x * y
    return multiplier

# 클로저 생성
double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))  # 10
print(triple(5))  # 15

클로저의 주요 이점

  • 데이터 은닉 및 캡슐화: 클로저를 사용하면 변수를 숨겨 상태를 안전하게 유지할 수 있다. 외부에서는 클로저 내부의 변수에 직접 접근할 수 없다.
  • 상태 유지: 함수가 실행될 때마다 상태를 유지할 수 있다. 클로저는 생성 시점의 환경을 "기억"하므로, 해당 상태를 계속해서 활용할 수 있다.
  • 커스터마이징 함수: 클로저를 이용하여 특정 파라미터가 미리 설정된 새로운 함수를 쉽게 만들 수 있다.

클로저의 주의점

  • 메모리 관리: 클로저가 외부 변수를 참조하고 있을 때는 해당 변수가 메모리에 계속 남아 있게 다. 이는 때로 메모리 사용에 영향을 줄 수 있으므로 주의가 필요하다.

클로저는 함수형 프로그래밍의 중요한 개념 중 하나로, 파이썬뿐만 아니라 JavaScript와 같은 다른 많은 언어에서도 널리 사용된다. 클로저를 통해 더 강력하고 유연한 프로그래밍이 가능해진다.

 

자유변수

자유변수(Free Variable)는 프로그래밍에서 중첩된 함수 구조 내에서 사용되는 용어로, 어떤 함수 내에서 사용되지만 그 함수 내에서 선언되지 않은 변수를 말한다. 즉, 자유변수는 해당 함수의 로컬 환경 외부에서 정의된 변수로, 주로 바깥쪽 함수의 로컬 변수가 이에 해당한다.

자유변수의 특징과 예시

자유변수는 특히 클로저에서 중요한 역할을 한다. 클로저는 함수의 코드와 그 함수가 참조하는 자유변수의 조합을 말하며, 이를 통해 함수가 자신이 생성될 당시의 환경을 "기억"할 수 있게 된다.

def outer():
    x = 10  # outer 함수의 로컬 변수

    def inner():
        return x  # x는 여기에서 자유변수

    return inner

my_func = outer()
print(my_func())  # 출력: 10

위 예제에서 inner() 함수는 outer() 함수의 로컬 변수 x를 사용한다. inner() 함수 내에서 x는 선언되지 않았기 때문에 x는 inner()에 대한 자유변수이다. outer() 함수가 종료된 후에도 inner() 함수는 x의 값을 "기억"하고 있으며, 이는 outer() 함수의 실행 컨텍스트가 사라진 후에도 x의 참조를 유지하기 때문이다.

자유변수의 중요성

  • 상태 유지: 클로저를 사용함으로써 함수가 종료된 후에도 특정 상태를 유지할 수 있다. 이는 함수형 프로그래밍에서 매우 유용한 특성이다.
  • 데이터 캡슐화: 자유변수를 사용하면, 함수 내부로 데이터를 은닉하고, 외부에서 직접 접근하는 것을 제한할 수 있다. 이는 객체지향 프로그래밍의 캡슐화와 비슷한 효과를 제공한다.
  • 함수 매개변수화: 자유변수를 통해 함수를 보다 유연하게 만들 수 있다. 예를 들어, 상태를 매개변수화하여 동일한 함수 로직을 다양한 상황에 적용할 수 있다.

자유변수는 파이썬의 클로저와 같은 고급 기능을 이해하고 활용하는 데 중요한 개념이다. 이를 통해 개발자는 보다 표현력 있고 유지 관리가 쉬운 코드를 작성할 수 있다.

 

nonlocal 선언

nonlocal 선언은 파이썬에서 중첩된 함수 내부에서, 바깥쪽 함수의 변수를 변경할 때 사용된다. 이 선언을 통해, 중첩된 내부 함수에서 바깥쪽 함수의 변수를 "자유변수(free variable)"로서 참조하고 그 값을 수정할 수 있다. 이는 중첩된 함수의 스코프 내에서 변수에 대한 바인딩을 지정하는 데 사용되며, global 키워드와 유사하게 작동하지만, nonlocal은 지역 변수에 대해, global은 모듈 수준의 전역 변수에 대해 작동한다.

nonlocal의 필요성

nonlocal은 특히 클로저와 같은 상황에서 유용하다. 클로저에서는 외부 함수의 변수를 내부 함수에서 읽을 수 있지만, 이 변수를 수정하려고 할 때 파이썬은 새로운 로컬 변수를 생성하려 한다. 이를 방지하고 외부 함수의 변수를 직접 수정하기 위해 nonlocal을 사용한다.

nonlocal 예제

아래 예시는 nonlocal 키워드의 사용법을 보여준다:

def outer():
    count = 0

    def inner():
        nonlocal count  # 바깥쪽 함수의 변수 count를 참조
        count += 1
        return count

    return inner

counter = outer()
print(counter())  # 출력: 1
print(counter())  # 출력: 2
print(counter())  # 출력: 3

위 코드에서 inner 함수는 outer 함수의 count 변수를 수정한다. nonlocal 선언 없이는 inner 함수 내에서 count += 1 수행 시 새로운 로컬 count 변수가 생성되어 UnboundLocalError가 발생할 것이다. nonlocal 선언을 통해 count 변수가 outer 함수의 count 변수임을 명시적으로 지정하므로, inner 함수는 outer의 count 변수를 성공적으로 수정할 수 있다.

주의사항

  • nonlocal은 해당 변수가 중첩된 함수의 외부 함수에 이미 존재하는 경우에만 사용할 수 있다. 최상위 수준에서는 nonlocal을 사용할 수 없으며, 해당 변수가 존재하지 않는 경우 에러가 발생한다.
  • nonlocal 변수는 반드시 이미 할당된 상태에서 수정이 가능하다. 즉, 변수는 외부 함수에서 반드시 초기화되어 있어야 한다.

nonlocal 선언은 파이썬의 함수 스코프와 클로저를 효과적으로 활용하게 해주는 중요한 도구이다. 이를 통해 중첩 함수 간의 변수 공유와 수정이 용이해져 복잡한 함수 로직을 보다 효율적으로 구현할 수 있다.

 

데커레이트된 함수 호출

파이썬에서 데커레이터를 사용하여 함수의 실행 시간을 측정하고, 해당 함수에 전달된 인수와 반환값을 출력하는 예제를 만들겠다. 이 예제는 두 부분으로 나눌 것이다: 하나는 데커레이터를 정의하는 모듈, 다른 하나는 이 모듈을 임포트해서 사용하는 실행 파일이다.

1. 데커레이터 정의 모듈 (decorator_module.py)

이 파일은 데커레이터를 정의한다. 데커레이터는 함수의 실행 시간을 측정하고, 함수 호출 시 전달된 인수와 반환값을 출력한다.

import time
import functools

def log_function_data(func):
    """데커레이터 함수: 실행 시간, 인수, 반환값을 로그로 출력합니다."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 함수 실행 시작 시간
        result = func(*args, **kwargs)  # 함수 호출 및 결과 저장
        end_time = time.time()  # 함수 실행 종료 시간
        duration = end_time - start_time  # 실행 시간 계산
        # 로그 출력
        print(f"Function {func.__name__!r} called with arguments {args} and kwargs {kwargs}")
        print(f"Returned value: {result}")
        print(f"Execution time: {duration:.6f} seconds")
        return result
    return wrapper

2. 실행 파일 (main.py)

이 파일에서는 decorator_module.py에서 정의한 데커레이터를 임포트하여 사용한다.

from decorator_module import log_function_data

@log_function_data
def add(a, b):
    """두 숫자를 더하는 함수"""
    return a + b

@log_function_data
def multiply(a, b):
    """두 숫자를 곱하는 함수"""
    return a * b

# 함수 호출
add(3, 4)
multiply(4, 5)

작동 과정 설명

  1. 데커레이터 정의: log_function_data는 인자로 함수 func를 받는다. 내부 함수 wrapper는 장식된 함수를 감싸고, 실행 전후로 필요한 로그를 출력한다. wrapper 함수는 가변 인자 args와 키워드 인자 *kwargs를 사용하여 장식된 함수가 받는 모든 인수를 처리할 수 있다.
  2. 시간 측정: wrapper 함수 내에서 time.time()을 호출하여 함수 실행 전후의 시간을 측정한다. 실행 시간은 이 두 시간의 차이로 계산된다.
  3. 로그 출력: 함수 호출 시 전달된 인수, 함수의 반환값, 그리고 실행 시간이 출력된다.
  4. 함수 호출: main.py에서는 add 함수와 multiply 함수에 log_function_data 데커레이터를 적용하다. 이 데커레이터는 함수가 호출될 때마다 정의된 로그를 출력하게 된다.

이 예제는 함수의 성능을 분석하거나 디버깅할 때 유용하게 사용할 수 있다. 데커레이터를 사용하는 이점은 코드 수정 없이도 기능을 추가하거나 변경할 수 있다는 것이다. 또한, 데커레이터를 사용하면 코드의 재사용성을 높이고, 코드의 가독성을 유지할 수 있다.

 

표준 라이브러리에서 제공하는 데커레이터

파이썬의 표준 라이브러리는 다양한 유용한 데커레이터를 제공하는데, 그 중에서도 functools.lru_cache()와 functools.singledispatch()는 매우 유용하게 쓰이는 데커레이터이다. 각각의 특성과 사용법에 대해 자세히 설명하겠다.

1. functools.lru_cache()

lru_cache()는 "Least Recently Used" 캐시를 구현한 데커레이터이다. 이 데커레이터는 함수의 호출 결과를 캐시하여, 동일한 인수로 재호출될 때 마다 함수를 다시 실행하지 않고, 캐시된 결과를 반환하여 성능을 향상시킨다. 특히 계산 비용이 높은 함수나 I/O 비용이 많이 드는 작업에 사용하기 적합하다.

사용 예제

from functools import lru_cache

@lru_cache(maxsize=32)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

# 첫 번째 호출에서는 계산을 수행
print(fib(10))  # 55

# 동일한 인수로 재호출 시 캐시된 결과를 사용
print(fib(10))  # 55 (재계산 없음)

주요 인자

  • maxsize: 캐시의 최대 크기를 지정한다. maxsize가 None이면 캐시의 크기 제한이 없다. maxsize를 소수점으로 제한하는 것이 효율적이다.
  • typed: 기본값은 False입니다. True로 설정하면 인수의 타입도 캐싱의 키에 포함되어, 같은 값이지만 타입이 다른 인수들을 구분한다.

2. functools.singledispatch()

singledispatch()는 함수 오버로딩을 지원하는 데커레이터이다. 파이썬은 기본적으로 함수 오버로딩을 지원하지 않지만, singledispatch()를 사용하면 첫 번째 인자의 타입에 따라 다른 함수를 호출하도록 할 수 있다. 이를 통해 같은 함수 이름에 여러 버전의 구현을 두고, 인자의 타입에 따라 적합한 함수가 실행되도록 할 수 있다.

사용 예제

from functools import singledispatch

@singledispatch
def say(data):
    print(f"data: {data}")

@say.register(int)
def _(data):
    print(f"int: {data}")

@say.register(list)
def _(data):
    print(f"list: {data}")

say("Hello")  # data: Hello
say(123)      # int: 123
say([1, 2, 3])  # list: [1, 2, 3]

주요 특성

  • @singledispatch로 정의된 기본 함수는 모든 타입에 적용되는 일반적인 구현을 제공한다.
  • @function_name.register(type)을 사용하여 특정 타입에 대한 구현을 추가한다.
  • 첫 번째 인자의 타입에 따라 알맞은 함수가 호출된다.

요약

  • lru_cache()는 함수의 결과를 캐시하여 반복된 호출에 대한 성능을 향상시키는 데 사용된다.
  • singledispatch()는 함수 오버로딩을 통해 같은 함수 이름에 대해 다른 타입의 인수에 따라 다른 구현을 할 수 있도록 지원한다.

이 두 데커레이터는 코드의 성능과 유지보수성을 크게 향상시키는 강력한 도구로 작용할 수 있다.

 

범용함수

"범용 함수"라는 용어는 보통 함수가 다양한 상황이나 맥락에서 널리 사용될 수 있음을 의미하는데, 특히 프로그래밍과 컴퓨터 과학 분야에서 이 용어가 자주 사용된다. 범용 함수는 특정한 문제나 타입에 국한되지 않고 여러 다른 타입이나 상황에 적용될 수 있는 함수를 지칭한다.

범용 함수의 특징

  • 다형성: 범용 함수는 여러 타입의 입력에 대해 작동할 수 있는 다형성을 지원한다. 이는 함수가 다양한 데이터 타입으로부터 유연하게 동작할 수 있음을 의미한다.
  • 재사용성: 한 번 정의된 범용 함수는 다양한 프로그램이나 모듈에서 재사용될 수 있다. 이로 인해 코드의 중복을 줄이고, 유지 관리를 용이하게 한다.
  • 확장성: 범용 함수는 새로운 타입이나 요구 사항에 맞게 쉽게 확장될 수 있다.

예시

예를 들어, 파이썬의 내장 함수 len()은 리스트, 문자열, 튜플 등 다양한 데이터 타입의 길이를 반환하는 범용 함수이다. len() 함수는 이러한 다양한 타입에 대해 동일하게 작동하므로, 범용성이 매우 높다고 볼 수 있다.

프로그래밍에서의 범용 함수

프로그래밍에서 범용 함수를 만드는 일반적인 방법은 다음과 같다:

  • 함수 오버로딩: 함수 오버로딩을 사용하여 같은 함수 이름에 대해 다른 입력 타입에 따른 여러 구현을 제공할 수 있다.
  • 템플릿 프로그래밍: C++과 같은 언어에서는 템플릿을 사용하여 타입에 독립적인 함수를 작성할 수 있다.
  • 다형성과 인터페이스: 객체 지향 프로그래밍에서는 인터페이스를 통해 다형성을 구현하여 다양한 객체 타입에 대해 동일한 함수를 적용할 수 있다.

이처럼 범용 함수는 프로그램의 유연성과 재사용성을 높이는 데 기여하며, 소프트웨어 개발에서 중요한 개념 중 하나이다.

 

누적된 데커레이터(accumulated decorators)

누적된 데커레이터(accumulated decorators)는 하나의 함수에 여러 개의 데커레이터를 순차적으로 적용하는 것을 의미한다. 이 방식을 통해 각각의 데커레이터가 제공하는 기능을 한 함수에 층층이 추가할 수 있으며, 각 데커레이터는 다른 데커레이터가 반환한 함수를 입력으로 받아 새로운 기능을 추가하거나 변경한다.

데커레이터의 적용 순서

누적된 데커레이터는 코드에서 가장 가까운 것부터 함수에 적용된다. 하지만 실제 실행 순서는 코드에서 가장 먼 것부터 실행된다. 즉, 가장 아래에 있는 데커레이터가 가장 먼저 실행되고, 그 결과가 위에 있는 데커레이터로 전달된다.

예제

다음은 파이썬에서 누적된 데커레이터를 사용한 예제이다:

def decorator1(func):
    def wrapper():
        print("Decorator 1 before call")
        result = func()
        print("Decorator 1 after call")
        return result
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2 before call")
        result = func()
        print("Decorator 2 after call")
        return result
    return wrapper

@decorator1
@decorator2
def say_hello():
    print("Hello!")

say_hello()

실행 결과

Decorator 1 before call
Decorator 2 before call
Hello!
Decorator 2 after call
Decorator 1 after call

작동 원리

  1. say_hello() 함수는 먼저 decorator2로 감싸진다.
  2. 그 결과(즉, decorator2의 wrapper 함수)가 decorator1로 다시 감싸진다.
  3. **say_hello()**를 호출하면 decorator1의 wrapper가 먼저 실행되고, 그 내부에서 decorator2의 wrapper가 실행된다.

누적된 데커레이터의 이점

  • 기능 확장: 누적된 데커레이터를 사용하면 한 함수에 여러 기능을 추가할 수 있다. 각 데커레이터는 독립적으로 작성될 수 있으므로 코드의 재사용성과 모듈성이 향상된다.
  • 코드 정리: 복잡한 로직을 여러 데커레이터로 나누어 관리할 수 있어, 각 데커레이터는 그 기능에만 집중할 수 있다. 이는 코드의 가독성과 유지보수성을 높인다.

누적된 데커레이터는 각 데커레이터가 독립적으로 기능을 수행하면서도 서로의 결과를 효과적으로 활용할 수 있게 해주는 강력한 방법으로, 파이썬에서 고급 기능 구현에 자주 사용된다.

 

매개변수화된 데커레이터

매개변수화된 데커레이터는 표준 데커레이터에 추가적인 인자를 받을 수 있도록 확장한 것이다. 이를 통해 데커레이터의 행동을 사용자가 지정한 값에 따라 동적으로 변경할 수 있으며, 데커레이터의 재사용성과 유연성이 크게 향상된다.

매개변수화된 데커레이터의 구조

매개변수화된 데커레이터를 작성하기 위해 일반적으로 세 단계의 함수가 필요하다:

  1. 외부 함수: 데커레이터의 매개변수를 받는다.
  2. 중간 함수: 실제로 작업할 함수를 받는다.
  3. 내부 함수: 실제 함수의 로직을 감싸고, 필요한 작업을 수행한다.

예제: 로깅 레벨을 매개변수로 받는 데커레이터

다음은 로깅 레벨을 매개변수로 받아, 해당 레벨에 따라 로그를 출력하는 매개변수화된 데커레이터의 예제이다.

from functools import wraps

def logger(level):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if level == "high":
                print(f"[HIGH LEVEL] Running {func.__name__} with arguments {args} {kwargs}")
            elif level == "medium":
                print(f"[MEDIUM LEVEL] Running {func.__name__}")
            else:
                print(f"[LOW LEVEL] Running function")

            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@logger(level="high")
def add(a, b):
    return a + b

@logger(level="low")
def subtract(a, b):
    return a - b

print(add(2, 3))
print(subtract(5, 3))

실행 결과

[HIGH LEVEL] Running add with arguments (2, 3) {}
5
[LOW LEVEL] Running function
2

작동 원리

  1. logger 함수는 level 매개변수를 받는다. 이 매개변수는 로깅의 세부 수준을 결정한다.
  2. decorator 함수는 장식될 함수 func를 인자로 받는다.
  3. wrapper 함수는 func 함수를 실행하기 전후에 필요한 추가 작업(여기서는 로그 출력)을 수행한다. 로그의 상세도는 level에 따라 달라진다.
  4. @wraps(func)를 사용함으로써 wrapper 함수가 원본 함수 func의 메타데이터(예: 함수명, 독스트링 등)를 유지하도록 한다.

장점

  • 동적인 기능 부여: 데커레이터에 매개변수를 도입함으로써 데커레이터의 기능을 호출 시점에 유연하게 변경할 수 있다.
  • 코드의 재사용성 향상: 같은 데커레이터를 다양한 방식으로 활용할 수 있어, 코드의 중복을 줄이고 재사용성을 높일 수 있다.

매개변수화된 데커레이터는 파이썬에서 함수의 기능을 확장하는 강력한 방법이며, 함수를 보다 유연하고 동적으로 제어할 수 있게 해준다.

 

클로저와 데커레이터의 심화 이해

1. 메모리 관리

클로저가 외부 변수를 참조하는 경우, 해당 변수는 클로저의 생명주기와 함께 활성 상태로 남게 된다. 이것은 클로저가 참조하는 변수가 함수의 실행이 끝난 후에도 메모리에서 해제되지 않음을 의미한다. 이로 인해 메모리 사용이 불필요하게 증가하거나 메모리 누수가 발생할 수 있다.

해결 방안:

  • 클로저가 더 이상 필요하지 않을 때는 클로저에 대한 모든 참조를 명시적으로 삭제한다.
  • 클로저 내부에서 사용하는 데이터의 크기를 최소화한다.
  • 클로저 사용이 끝났을 때 수동으로 리소스를 정리하는 메커니즘을 구현한다.

2. 재귀 함수와 데커레이터

재귀 함수에 데커레이터를 적용할 경우, 데커레이터가 재귀 호출마다 적용되어 성능 저하를 일으킬 수 있다. 데커레이터가 추가적인 처리나 메모리 할당을 수행하기 때문에 재귀 깊이가 깊어질수록 이러한 비용이 증가한다.

해결 방안:

  • 재귀 함수에 데커레이터를 적용할 때는 성능을 면밀히 분석하고 필요하다면 다른 방식(예: 반복문 활용)으로 로직을 구현한다.
  • 데커레이터 로직을 최적화하여 추가적인 비용을 최소화한다.

3. 데커레이터와 스레드 안전성

데커레이터 내부에서 공유 자원에 접근하거나 수정하는 경우, 여러 스레드가 동시에 접근할 때 자원의 일관성이 깨질 수 있다. 이는 데이터 손상이나 예상치 못한 버그를 일으킬 수 있다.

해결 방안:

  • 스레드 로컬 스토리지를 사용하여 각 스레드가 자신만의 데이터 복사본을 가지게 한다.
  • 필요한 경우 락을 사용하여 자원 접근을 동기화하고, 안전하게 자원을 공유된다.

 

모든 코드는 github에 저장되어 있습니다.

https://github.com/SeongUk18/python

728x90

'Python study' 카테고리의 다른 글

파이썬스러운 객체(Pythonic object)  (0) 2024.05.20
객체 참조, 가변성, 재활용  (0) 2024.05.13
일급 함수 디자인 패턴  (0) 2024.05.08
일급 함수  (0) 2024.05.06
텍스트(text)와 바이트(byte)  (0) 2024.05.04