English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Explicación detallada del uso avanzado de decoradores en Python

En Python, los decoradores generalmente se utilizan para modificar funciones, implementar funciones comunes y lograr el objetivo de la reutilización de código. Antes de la definición de la función se agrega @xxxx, y luego la función se inyecta con ciertos comportamientos, ¡es muy mágico! Sin embargo, esto no es más que un azúcar sintáctico.

Escena

Supongamos que hay algunas funciones de trabajo, que se utilizan para realizar diferentes procesamientos de datos:

def work_bar(data):
  pass
def work_foo(data):
  pass

Queremos que se llame a la función antes/¿Qué hacer si se desea que se escriba el registro después de la salida?

Solución estúpida

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

¿Qué pasa si hay varios lugares de código que llaman? ¡Sólo piénsalo y se asusta!

Envolvente de función

La solución estúpida no es más que mucho código redundante, cada vez que se llama a la función se debe escribir logging. Se puede encapsular esta lógica redundante en una nueva función:

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

De esta manera, cada vez que se llame a smart_work_bar:

smart_work_bar(1)
# ...
smart_work_bar(some_data)

Closures genéricos

Parece bastante perfecto... Sin embargo, ¿también hay que implementar de nuevo smart_work_foo cuando work_foo también tiene la misma necesidad? Esto claramente no es científico!

No se apresure, podemos usar closures:

def log_call(func):
  def proxy(*args, **kwargs):
    logging.info('inicio de llamada: {name}'.format(name=func.func_name))
    result = func(*args, **kwargs)
    logging.info('llamada finalizada: {name}'.format(name=func.func_name))
    return result
  return proxy

Esta función recibe un objeto de función (función enmascarada) como parámetro, devuelve una función proxy. Al llamar a la función proxy, primero se imprime el registro, luego se llama a la función enmascarada, se completa la llamada y finalmente se devuelve el resultado de la llamada. De esta manera, ¿no se alcanza el objetivo de la generalización? - log_call puede manejar fácilmente cualquier función enmascarada.

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)

el1En la línea, log_call recibe el parámetro work_bar, devuelve una función proxy y la asigna a smart_work_bar.4En la línea, se llama a smart_work_bar, es decir, la función proxy, se imprime el registro primero, luego se llama a func, es decir, work_bar, y finalmente se imprime el registro. Nota que en la función proxy, func y el objeto work_bar pasado están estrechamente relacionados, esto es un closure.

Además, puede sobrescribir el nombre de la función que se está enmascarando, y usar un nombre nuevo con prefijo smart_ puede parecer algo cansado:

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

Azúcar sintáctico

Veamos el siguiente código primero:

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

Aunque el código ya no tiene redundancia, no es lo suficientemente intuitivo de ver. En este momento, entra en juego el azúcar sintáctico~~~

@log_call
def work_bar(data):
  pass

Por lo tanto, ten en cuenta (¡ponte en modo de atención!), el único propósito de @log_call aquí es: informar al compilador de Python para insertar el código work_bar = log_call(work_bar).

Decorador de evaluación

Antes de nada, ¿cómo crees que funciona el decorador eval_now?

def eval_now(func):
  return func()

parece extraño, ¿no? No se define una función proxy, ¿es un decorador?

@eval_now
def foo():
  devolver 1
print foo

Este código imprime1es decir, evaluar la llamada a la función. ¿Entonces, ¿para qué sirve? Escribir directamente foo = 1¿No está bien? En este ejemplo simple, por supuesto que sí. Vamos a ver un ejemplo más complejo: inicializar un objeto de registro:

# otro código antes...
# formato de registro
formatter = logging.Formatter(
  '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
  '%Y-%m-%d %H:%M:%S',
)
# gestor stdout
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
stdout_handler.setLevel(logging.DEBUG)
# gestor stderr
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
stderr_handler.setLevel(logging.ERROR)
# objeto logger
logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)
# otro código después...

Usando el modo eval_now:

# otro código antes...
@eval_now
def logger():
  # formato de registro
  formatter = logging.Formatter(
    '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
    '%Y-%m-%d %H:%M:%S',
  )
  # gestor stdout
  stdout_handler = logging.StreamHandler(sys.stdout)
  stdout_handler.setFormatter(formatter)
  stdout_handler.setLevel(logging.DEBUG)
  # gestor stderr
  stderr_handler = logging.StreamHandler(sys.stderr)
  stderr_handler.setFormatter(formatter)
  stderr_handler.setLevel(logging.ERROR)
  # objeto logger
  logger = logging.Logger(__name__)
  logger.setLevel(logging.DEBUG)
  logger.addHandler(stdout_handler)
  logger.addHandler(stderr_handler)
  devolver logger
# otro código después...

Los dos bloques de código tienen el mismo objetivo, pero el segundo es obviamente más claro y tiene un estilo de bloque de código. Lo más importante es que la llamada a la función se inicializa en el espacio de nombres local, evitando que las variables temporales (como formatter, por ejemplo) contaminen el espacio de nombres externo (como el espacio de nombres global).

Decorador con parámetros

Definir un decorador para registrar llamadas a funciones lentas:

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('llamada lenta: {name} en {seconds}s'.format(
      name=func.func_name,
      seconds=seconds,
    ))
    return result
  return proxy

el3,5La línea se mide en el tiempo antes y después de la llamada a la función, el7Calcula el tiempo de llamada de la línea, si el tiempo de duración es mayor de un segundo, se imprime una advertencia.

@log_slow_call
def sleep_seconds(seconds):
  time.sleep(seconds)
sleep_seconds(0.1)  # No hay salida de registro
sleep_seconds(2)  # Imprimir el registro de advertencia

Sin embargo, el establecimiento del umbral siempre debe decidirse en función del caso, diferentes funciones pueden establecer diferentes valores. Si fuera posible parametrizar el umbral:

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('llamada lenta: {name} en {seconds}s'.format(
      name=func.func_name,
      seconds=seconds,
    ))
    return result
  return proxy

Sin embargo, la sintaxis de azúcar @xxxx siempre llama al decorador con el función decorada como parámetro, lo que significa que no hay oportunidad de pasar el parámetro threshold. ¿Qué hacer? — Usar una clausura para encapsular el parámetro 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('llamada lenta: {name} en {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)

De esta manera, log_slow_call(threshold=0.5) devuelve la función decorator, que tiene la variable de cierre threshold con un valor de 0.5。El decorador vuelve a decorar sleep_seconds.

Usar el valor predeterminado del umbral, pero no se puede omitir la llamada a la función:

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

Si Virgo no está contento con la primera línea de estos paréntesis, puede mejorarse así:

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('llamada lenta: {name} en {seconds}s'.format(
        name=func.func_name,
        seconds=seconds,
      ))
      return result
    return proxy
  if func is None:
    return decorator
  else:
    return decorator(func)

Esta escritura es compatible con dos usos diferentes, el uso A con el valor predeterminado del umbral (sin llamada) y el uso B con un umbral personalizado (con llamada).

# Caso 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)

En el caso de uso A, lo que ocurre es log_slow_call(sleep_seconds), es decir, el parámetro func no está vacío, lo que significa que se llama directamente al decorator para envolverlo y devolverlo (el umbral es el predeterminado).

En el caso de uso B, lo primero que ocurre es log_slow_call(threshold=0.5),el parámetro func está vacío, por lo que se devuelve el nuevo decorador decorator, asociado a la variable de cierre threshold, con un valor de 0.5;luego, el decorador decora la función sleep_seconds, es decir, decorator(sleep_seconds). Nota que en este momento, el valor asociado a threshold es 0.5,para completar la personalización.

Puede que hayas notado que es mejor usar la forma de llamada con parámetros nombrados aquí, ya que usar parámetros por posición sería feo:

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

Claro, utilizar parámetros nombrados en las llamadas a funciones es una excelente práctica, más aún cuando hay muchos parámetros.

Decorador inteligente

La forma de escribir presentada en el capítulo anterior tiene una capa de anidación较多, si cada decorador similar se implementa de esta manera, es bastante difícil (no hay suficiente mente), también es fácil cometer errores.

Supongamos que hay un decorador inteligente smart_decorator que decora el decorador log_slow_call, podemos obtener la misma capacidad. De esta manera, la definición de log_slow_call se volverá más clara y será más fácil de implementar:

@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('llamada lenta: {name} en {seconds}s'.format(
      name=func.func_name,
      seconds=seconds,
    ))
    return result
  return proxy

Después de abrir la mente, ¿cómo se implementa smart_decorator? En realidad, es simple:

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

Después de que smart_decorator se haya implementado, la hipótesis se ha establecido! En este momento, log_slow_call, es decorator_proxy(externo), la variable de cierre asociada decorator es el log_slow_call definido al principio de esta sección (para evitar ambigüedades, se llama real_log_slow_call). log_slow_call admite los siguientes usos:

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

En el caso de uso A, se ejecuta decorator_proxy(sleep_seconds)(externo), func no es nulo, kwargs está vacío; se ejecuta directamente 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(),func及kwargs均为空,返回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),阈值实现自定义!

Declaración: el contenido de este artículo se obtiene de la red, es propiedad del autor original, el contenido se contribuye y carga de manera autónoma por los usuarios de Internet, este sitio no posee los derechos de propiedad, no se ha realizado un procesamiento editorial humano y no asume ninguna responsabilidad legal relacionada. Si encuentra contenido sospechoso de infracción de derechos de autor, le invitamos a enviar un correo electrónico a: notice#oldtoolbag.com (al enviar un correo electrónico, reemplace # con @ para denunciar y proporcione pruebas relacionadas. Una vez que se verifique, este sitio eliminará inmediatamente el contenido sospechoso de infracción de derechos de autor.)

Te gustará