>  기사  >  백엔드 개발  >  Python 데코레이터를 이해하려면 이 기사를 읽는 것으로 충분합니다.

Python 데코레이터를 이해하려면 이 기사를 읽는 것으로 충분합니다.

高洛峰
高洛峰원래의
2017-02-17 11:07:321620검색

Python 데코레이터에 대해 이야기하기 전에 먼저 예를 들어보겠습니다. 비록 다소 지저분하지만 데코레이터라는 주제와 매우 관련이 있습니다.

모든 사람이 가지고 있는 속옷의 주된 기능은 부끄러움을 가리는 것이지만, 겨울에는 바람과 추위로부터 우리를 지켜줄 수 없습니다. 어떻게 해야 할까요? 우리가 생각한 방법 중 하나는 속옷을 더 두껍고 길게 변형하는 것이었습니다. 이렇게 하면 부끄러움을 가려주는 기능뿐만 아니라 보온성도 제공됩니다. 그러나 속옷을 바지로 변형한 후에는 문제가 있었습니다. , , 여전히 수치심을 가리는 기능이 있지만 본질적으로 더 이상 실제 속옷이 아닙니다. 그래서 똑똑한 사람들은 바지를 발명하고 속옷에 영향을 주지 않고 바지를 속옷 위에 직접 올려 놓았습니다. 데코레이터는 속옷의 기능에 영향을 주지 않으면서 우리 몸에 따뜻함을 주는 바지와 같습니다.

데코레이터에 대해 이야기하기 전에 먼저 한 가지를 이해해야 합니다. Python의 함수는 Java 및 C++와 다릅니다. Python의 함수는 일반 변수처럼 매개변수로 다른 함수에 전달될 수 있습니다.

def foo():
    print("foo")def bar(func):
    func()bar(foo)

공식적으로 다시 주제로 돌아왔습니다. 데코레이터는 본질적으로 다른 함수나 클래스가 코드를 수정하지 않고도 추가 기능을 추가할 수 있게 해주는 Python 함수 또는 클래스입니다. 데코레이터의 반환 값도 함수/클래스 개체입니다. 로그 삽입, 성능 테스트, 트랜잭션 처리, 캐싱, 권한 확인 등과 같은 교차 요구 사항이 있는 시나리오에서 자주 사용됩니다. 데코레이터는 이러한 문제를 해결하는 데 탁월한 디자인입니다. 데코레이터를 사용하면 함수 자체와 관련이 없는 유사한 코드를 대량으로 추출하여 계속해서 재사용할 수 있습니다. 간단히 말해서 데코레이터의 목적은 기존 객체에 추가 기능을 추가하는 것입니다.

먼저 간단한 예를 살펴보겠습니다. 실제 코드는 이보다 훨씬 더 복잡할 수 있습니다.

def foo():
    print('i am foo')

이제 함수의 실행 로그를 기록해야 하는 새로운 요구 사항이 있습니다. , 그래서 로그 코드를 추가합니다:

def foo():
    print('i am foo')
    logging.info("foo is running")

bar()와 bar2() 함수의 요구 사항이 비슷하면 어떻게 될까요? bar 함수에 또 다른 로깅을 작성하시겠습니까? 이로 인해 유사한 코드가 많이 생성됩니다. 이를 수행하고 새로운 기능을 재정의할 수 있습니다. 구체적으로 로그를 처리한 후 실제 비즈니스 코드가 실행됩니다.

def use_logging(func):
    logging.warn("%s is running" % func.__name__)
    func()def foo():
    print('i am foo')use_logging(foo)

이 로직은 문제가 없습니다. 함수가 구현되었지만 호출하면 더 이상 실제 비즈니스 로직인 foo 함수를 호출하지 않고 원래 코드 구조를 파괴하는 use_logging 함수로 대체합니다. 매번 호출해야 합니다. 원래 foo 함수를 use_logging 함수에 매개변수로 전달해야 할 때마다 더 좋은 방법이 있나요? 물론 그렇습니다. 대답은 데코레이터입니다.

간단한 데코레이터

def use_logging(func):

    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()   # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
    return wrapperdef foo():
    print('i am foo')foo = use_logging(foo)  # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于  foo = wrapperfoo()                   # 执行foo()就相当于执行 wrapper()

use_logging은 데코레이터로, 실제 비즈니스 로직을 실행하는 func 함수를 감싸는 일반 함수입니다. foo도 use_logging, use_logging으로 장식되어 있는 것 같습니다. 함수를 반환하며 이 함수의 이름은 래퍼입니다. 이 예에서는 함수가 들어오고 나가는 것을 단면 지향 프로그래밍이라고 합니다.

def use_logging(func):

    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()
    return wrapper@use_loggingdef foo():
    print("i am foo")foo()

위에 표시된 것처럼 @를 사용하면 foo = use_logging(foo) 문장을 생략하고 foo()를 직접 호출하여 원하는 결과를 얻을 수 있습니다. foo() 함수는 어떤 식으로든 수정할 필요가 없다는 것을 보셨나요? 정의된 곳에 데코레이터를 추가하기만 하면 호출은 여전히 ​​이전과 동일합니다. 다른 유사한 함수가 있으면 계속 호출할 수 있습니다. 함수를 반복적으로 수정하거나 새 패키지를 추가하지 않고도 함수를 장식할 수 있습니다. 이러한 방식으로 프로그램의 재사용성을 향상시키고 프로그램의 가독성을 높입니다.

파이썬에서 데코레이터를 사용하기 편리한 이유는 파이썬 함수를 일반 객체처럼 다른 함수에 매개변수로 전달할 수도 있고, 다른 변수에 할당할 수도 있고, 반환값으로 사용할 수도 있고, 다른 함수 내에서 정의됩니다.

*args, **kwargs

비즈니스 로직 함수인 foo에 매개변수가 필요한 경우에는 어떻게 되는지 묻는 사람이 있을 수 있습니다. 예:

def foo(name):
    print("i am %s" % name)

래퍼 함수를 ​​정의할 때 매개변수를 지정할 수 있습니다.

def wrapper(name):
        logging.warn("%s is running" % func.__name__)
        return func(name)
    return wrapper

这样 foo 函数定义的参数就可以定义在 wrapper 函数中。这时,又有人要问了,如果 foo 函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道 foo 到底有多少个参数时,我们可以用 *args 来代替:

def wrapper(*args):
        logging.warn("%s is running" % func.__name__)
        return func(*args)
    return wrapper

如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:

def foo(name, age=None, height=None):
    print("I am %s, age %s, height %s" % (name, age, height))

这时,你就可以把 wrapper 函数指定关键字函数:

def wrapper(*args, **kwargs):
        # args是一个数组,kwargs一个字典
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator@use_logging(level="warn")def foo(name='foo'):
    print("i am %s" % name)foo()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

@use_logging(level="warn")等价于@decorator

类装饰器

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')@Foodef bar():
    print ('bar')bar()

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、__name__、参数列表,先看例子:

# 装饰器def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'with_logging'
        print func.__doc__       # 输出 None
        return func(*args, **kwargs)
    return with_logging# 函数@loggeddef f(x):
   """does some math"""
   return x + x * xlogged(f)

不难发现,函数 f 被with_logging取代了,当然它的docstring,__name__就是变成了with_logging函数的信息了。好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

from functools import wrapsdef logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'f'
        print func.__doc__       # 输出 'does some math'
        return func(*args, **kwargs)
    return with_logging@loggeddef f(x):
   """does some math"""
   return x + x * x

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

f = a(b(c(f)))

zhijun liu首发于知乎专栏Python之禅,原文地址:https://zhuanlan.zhihu.com/p/24900548

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.