>  기사  >  백엔드 개발  >  Python 데코레이터의 자세한 사용법 소개(코드 예)

Python 데코레이터의 자세한 사용법 소개(코드 예)

不言
不言앞으로
2019-02-25 10:33:342681검색

이 글은 Python 데코레이터의 자세한 사용법 소개(코드 예제)를 제공합니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

Python에서 데코레이터는 일반적으로 공용 함수를 구현하고 코드 재사용을 달성하기 위해 함수를 장식하는 데 사용됩니다. 함수 정의 앞에 @xxxx를 추가하면 함수가 특정 동작을 주입하는데, 정말 놀랍습니다! 그러나 이것은 단지 구문상의 설탕일 뿐입니다.

Scenario

데이터를 다르게 처리하는 데 사용되는 몇 가지 작업 기능이 있다고 가정합니다.

def work_bar(data):
    pass


def work_foo(data):
    pass

우리는 함수 호출 전후에 로그를 출력하려면 어떻게 해야 하나요?

바보의 솔루션

logging.info('begin call work_bar')
work_bar(1)
logging.info('call work_bar done')

코드 호출이 여러 개 있으면 어떻게 되나요? 생각만 해도 무서워요!

함수 패키징

바보의 해결책은 코드 중복이 너무 많아서 각 함수 호출에 대해 로깅을 작성해야 하는 것뿐입니다. 중복 논리의 이 부분은 새로운 함수로 캡슐화될 수 있습니다: logging。可以把这部分冗余逻辑封装到一个新函数里:

def smart_work_bar(data):
    logging.info('begin call: work_bar')
    work_bar(data)
    logging.info('call doen: work_bar')

这样,每次调用smart_work_bar即可:

smart_work_bar(1)

# ...

smart_work_bar(some_data)

通用闭包

看上去挺完美……然而,当work_foo也有同样的需要时,还要再实现一遍smart_work_foo吗?这样显然不科学呀!

别急,我们可以用闭包:

def log_call(func):
    def proxy(*args, **kwargs):
        logging.info('begin call: {name}'.format(name=func.func_name))
        result = func(*args, **kwargs)
        logging.info('call done: {name}'.format(name=func.func_name))
        return result
    return proxy

这个函数接收一个函数对象(被代理函数)作为参数,返回一个代理函数。调用代理函数时,先输出日志,然后调用被代理函数,调用完成后再输出日志,最后返回调用结果。这样,不就达到通用化的目的了吗?——对于任意被代理函数funclog_call均可轻松应对。

smart_work_bar = log_call(work_bar)
smart_work_foo = log_call(work_foo)

smart_work_bar(1)
smart_work_foo(1)

# ...

smart_work_bar(some_data)
smart_work_foo(some_data)

1行中,log_call接收参数work_bar,返回一个代理函数proxy,并赋给smart_work_bar。第4行中,调用smart_work_bar,也就是代理函数proxy,先输出日志,然后调用func也就是work_bar,最后再输出日志。注意到,代理函数中,func与传进去的work_bar对象紧紧关联在一起了,这就是闭包

再提一下,可以覆盖被代理函数名,以smart_为前缀取新名字还是显得有些累赘:

work_bar = log_call(work_bar)
work_foo = log_call(work_foo)

work_bar(1)
work_foo(1)

语法糖

先来看看以下代码:

def work_bar(data):
    pass
work_bar = log_call(work_bar)


def work_foo(data):
    pass
work_foo = log_call(work_foo)

虽然代码没有什么冗余了,但是看是去还是不够直观。这时候,语法糖来了~~~

@log_call
def work_bar(data):
    pass

因此,注意一点(划重点啦),这里@log_call的作用只是:告诉Python编译器插入代码work_bar = log_call(work_bar)

求值装饰器

先来猜猜装饰器eval_now有什么作用?

def eval_now(func):
    return func()

看上去好奇怪哦,没有定义代理函数,算装饰器吗?

@eval_now
def foo():
    return 1

print foo

这段代码输出1,也就是对函数进行调用求值。那么到底有什么用呢?直接写foo = 1不行么?在这个简单的例子,这么写当然可以啦。来看一个更复杂的例子——初始化一个日志对象:

# some other code before...

# log format
formatter = logging.Formatter(
    '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
    '%Y-%m-%d %H:%M:%S',
)

# stdout handler
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
stdout_handler.setLevel(logging.DEBUG)

# stderr handler
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
stderr_handler.setLevel(logging.ERROR)

# logger object
logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)

# again some other code after...

eval_now的方式:

# some other code before...

@eval_now
def logger():
    # log format
    formatter = logging.Formatter(
        '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
        '%Y-%m-%d %H:%M:%S',
    )

    # stdout handler
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(formatter)
    stdout_handler.setLevel(logging.DEBUG)

    # stderr handler
    stderr_handler = logging.StreamHandler(sys.stderr)
    stderr_handler.setFormatter(formatter)
    stderr_handler.setLevel(logging.ERROR)

    # logger object
    logger = logging.Logger(__name__)
    logger.setLevel(logging.DEBUG)
    logger.addHandler(stdout_handler)
    logger.addHandler(stderr_handler)

    return logger

# again some other code after...

两段代码要达到的目的是一样的,但是后者显然更清晰,颇有代码块的风范。更重要的是,函数调用在局部名字空间完成初始化,避免临时变量(如formatter等)污染外部的名字空间(比如全局)。

带参数装饰器

定义一个装饰器,用于记录慢函数调用:

def log_slow_call(func):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > 1:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy

35行分别在函数调用前后采样当前时间,第7行计算调用耗时,耗时大于一秒输出一条警告日志。

@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)

sleep_seconds(0.1)  # 没有日志输出

sleep_seconds(2)    # 输出警告日志

然而,阈值设置总是要视情况决定,不同的函数可能会设置不同的值。如果阈值有办法参数化就好了:

def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy

然而,@xxxx语法糖总是以被装饰函数为参数调用装饰器,也就是说没有机会传递threshold参数。怎么办呢?——用一个闭包封装threshold参数:

def log_slow_call(threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    return decorator


@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)

这样,log_slow_call(threshold=0.5)调用返回函数decorator,函数拥有闭包变量threshold,值为0.5decorator再装饰sleep_seconds

采用默认阈值,函数调用还是不能省略:

@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)

处女座可能会对第一行这对括号感到不爽,那么可以这样改进:

def log_slow_call(func=None, threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    if func is None:
        return decorator
    else:
        return decorator(func)

这种写法兼容两种不同的用法,用法A默认阈值(无调用);用法B自定义阈值(有调用)。

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)


# Case B
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)

用法A中,发生的事情是log_slow_call(sleep_seconds),也就是func参数是非空的,这是直接调decorator

# Case B-
@log_slow_call(None, 0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
이러한 방식으로 smart_work_bar는 매번 호출될 수 있습니다: #🎜🎜#
@smart_decorator
def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
#🎜🎜#General 클로저 패키지 #🎜🎜##🎜🎜#은 완벽해 보입니다... 하지만 work_foo에도 동일한 요구 사항이 있는 경우 smart_work_foo를 다시 구현해야 합니까? 이것은 분명히 비과학적입니다! #🎜🎜##🎜🎜#걱정하지 마세요. 클로저를 사용할 수 있습니다. #🎜🎜#
def smart_decorator(decorator):

    def decorator_proxy(func=None, **kwargs):
        if func is not None:
            return decorator(func=func, **kwargs)

        def decorator_proxy(func):
            return decorator(func=func, **kwargs)

        return decorator_proxy

    return decorator_proxy
#🎜🎜#이 함수는 함수 객체(프록시 함수)를 매개변수로 받고 프록시 함수를 반환합니다. 프록시 함수를 호출하면 로그가 먼저 출력되고, 프록시 함수가 호출된 후 호출이 완료된 후 로그가 출력되고, 마지막으로 호출 결과가 반환됩니다. 이런 식으로 일반화의 목적을 달성하는 것이 아닌가? ——모든 프록시 함수 func, log_call을 쉽게 처리할 수 있습니다. #🎜🎜#
# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)
#🎜🎜#1행에서 log_callwork_bar 매개변수를 수신하고 프록시프록시 함수를 반환합니다. code>, smart_work_bar에 할당됩니다. 4행에서 proxy 프록시 함수인 smart_work_bar를 호출하고 먼저 로그를 출력한 다음 func를 호출합니다. > 역시 work_bar이고, 최종적으로 로그를 출력합니다. 프록시 함수에서 func는 전달된 work_bar 객체와 밀접하게 관련되어 있습니다. 이는 #🎜🎜#closure#🎜🎜#입니다. #🎜🎜##🎜🎜# 다시 한 번 프록시 함수 이름을 덮어쓸 수 있지만 새 이름 앞에 smart_를 붙이는 것은 여전히 ​​약간 번거롭습니다. #🎜🎜#
# Case B
# Same to Case A
@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
#🎜🎜#Syntactic sugar# 🎜🎜##🎜🎜#먼저 다음 코드를 살펴보겠습니다. #🎜🎜#
# Case C
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
#🎜🎜#코드에 중복성이 없지만 여전히 충분히 직관적이지 않습니다. 이때 구문슈가 온다~~#🎜🎜#rrreee#🎜🎜#그래서 @log_call 함수 한가지 주목하세요(#🎜🎜#Emphasis #🎜🎜#) 여기에는 work_bar = log_call(work_bar) 코드를 삽입하도록 Python 컴파일러에 지시합니다. #🎜🎜##🎜🎜#평가 데코레이터#🎜🎜##🎜🎜#먼저 eval_now 데코레이터가 무엇을 하는지 추측해 볼까요? #🎜🎜#rrreee#🎜🎜#이상한 것 같습니다. 프록시 함수가 정의되어 있지 않습니다. 데코레이터로 간주됩니까? #🎜🎜#rrreee#🎜🎜#이 코드는 함수를 호출하고 평가하는 1을 출력합니다. 그럼 무슨 소용이 있나요? foo = 1을 직접 쓰면 안 되나요? 이 간단한 예에서는 물론 이렇게 작성할 수도 있습니다. 좀 더 복잡한 예를 살펴보겠습니다. 로그 객체 초기화: #🎜🎜#rrreee#🎜🎜#eval_now 사용: #🎜🎜#rrreee#🎜🎜#다음을 달성하기 위해 필요한 두 가지 코드는 무엇입니까? 목적은 동일하지만 후자가 확실히 더 명확하고 코드 블록 스타일을 갖습니다. 더 중요한 것은 임시 변수(예: formatter 등)가 외부 네임스페이스(예: 전역)를 오염시키지 않도록 함수 호출이 로컬 네임스페이스에서 초기화된다는 것입니다. #🎜🎜##🎜🎜#매개변수가 있는 데코레이터#🎜🎜##🎜🎜#느린 함수 호출을 기록하기 위한 데코레이터 정의: #🎜🎜#rrreee#🎜🎜#35 는 함수 호출 전후의 현재 시간을 샘플링합니다. 7행은 호출 시간이 1초 이상 걸리는 경우 경고 로그를 출력합니다. #🎜🎜#rrreee#🎜🎜#그러나 임계값 설정은 항상 상황에 따라 다르며, 기능마다 다른 값을 설정할 수 있습니다. 임계값을 매개변수화하는 방법이 있다면 좋을 것입니다: #🎜🎜#rrreee#🎜🎜# 그러나 @xxxx 구문 설탕은 항상 장식된 함수를 매개변수로 사용하여 데코레이터를 호출합니다. threshold 매개변수를 전달할 기회가 없습니다. 무엇을 해야 할까요? ——클로저를 사용하여 threshold 매개변수를 캡슐화합니다: #🎜🎜#rrreee#🎜🎜#이런 식으로 log_slow_call(threshold=0.5)는 반환 함수를 호출합니다. 데코레이터 code>, 이 함수에는 <code>0.5 값을 가진 클로저 변수 threshold가 있습니다. 장식자sleep_seconds를 장식합니다. #🎜🎜##🎜🎜#기본 임계값을 사용하면 함수 호출을 생략할 수 없습니다: #🎜🎜#rrreee#🎜🎜#Virgos는 첫 번째 줄에 있는 괄호 쌍이 불편할 수 있으니 이렇게 개선하면 됩니다. : #🎜🎜# rrreee#🎜🎜#이 쓰기 방법은 두 가지 다른 사용법과 호환됩니다. 사용 A 기본 임계값(호출 없음) 사용 B 사용자 정의 임계값(호출 포함) ). #🎜🎜#rrreee#🎜🎜#A 사용법에서 log_slow_call(sleep_seconds)가 발생합니다. 즉, func 매개변수가 -empty - 데코레이터를 직접 호출하여 래핑하고 반환합니다(임계값이 기본값임). #🎜🎜#

用法B中,先发生的是log_slow_call(threshold=0.5)func参数为空,直接返回新的装饰器decorator,关联闭包变量threshold,值为0.5;然后,decorator再装饰函数sleep_seconds,即decorator(sleep_seconds)。注意到,此时threshold关联的值是0.5,完成定制化。

你可能注意到了,这里最好使用关键字参数这种调用方式——使用位置参数会很丑陋:

# Case B-
@log_slow_call(None, 0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)

当然了,函数调用尽量使用关键字参数是一种极佳实践,含义清晰,在参数很多的情况下更是如此。

智能装饰器

上节介绍的写法,嵌套层次较多,如果每个类似的装饰器都用这种方法实现,还是比较费劲的(脑子不够用),也比较容易出错。

假设有一个智能装饰器smart_decorator,修饰装饰器log_slow_call,便可获得同样的能力。这样,log_slow_call定义将变得更清晰,实现起来也更省力啦:

@smart_decorator
def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy

脑洞开完,smart_decorator如何实现呢?其实也简单:

def smart_decorator(decorator):

    def decorator_proxy(func=None, **kwargs):
        if func is not None:
            return decorator(func=func, **kwargs)

        def decorator_proxy(func):
            return decorator(func=func, **kwargs)

        return decorator_proxy

    return decorator_proxy

smart_decorator实现了以后,设想就成立了!这时,log_slow_call,就是decorator_proxy(外层),关联的闭包变量decorator是本节最开始定义的log_slow_call(为了避免歧义,称为real_log_slow_call)。log_slow_call支持以下各种用法:

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)

用法A中,执行的是decorator_proxy(sleep_seconds)(外层),func非空,kwargs为空;直接执行decorator(func=func, **kwargs),即real_log_slow_call(sleep_seconds),结果是关联默认参数的proxy

# Case B
# Same to Case A
@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)

用法B中,先执行decorator_proxy()funckwargs均为空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(func, **kwargs),等价于real_log_slow_call(sleep_seconds),效果与用法A一致。

# Case C
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)

用法C中,先执行decorator_proxy(threshold=0.5)func为空但kwargs非空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(sleep_seconds, **kwargs),等价于real_log_slow_call(sleep_seconds, threshold=0.5),阈值实现自定义!

위 내용은 Python 데코레이터의 자세한 사용법 소개(코드 예)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제