He estado estudiando cómo crear sus propios decoradores y se dio el siguiente ejemplo:

def counter(func):
  def wrapper(*args, **kwargs):
    wrapper.count += 1
    # Call the function being decorated and return the result
    return wrapper.count
  wrapper.count = 0
  # Return the new decorated function
  return wrapper

# Decorate foo() with the counter() decorator
@counter
def foo():
  print('calling foo()')
  
foo()
foo()

print('foo() was called {} times.'.format(foo.count))

No entiendo la lógica de ese código.

  1. ¿Cómo puedo hacer referencia a una función dentro de sí misma (wrapper.count)?
  2. ¿Cómo envoltorio cuenta el método antes de definir el envoltorio?
  3. ¿No debería ejecutarse la línea wrapper.count = 0 cada vez que llamo a foo ()?
4
Federico Vega 25 jun. 2020 a las 16:29

3 respuestas

La mejor respuesta
  1. ¿Cómo puedo hacer referencia a una función dentro de sí mismo (wrapper.count)?

Los cuerpos de las funciones se ejecutan solo cuando lo llamas. Para ese momento, la función ya está definida, así que esto es lo que lo hace posible. Lo siguiente no le dará ningún error, a menos que lo llame:

>>> def foo():
...     non_existing_function()
...

Y cada vez que ingrese el cuerpo de foo, foo ya está definido, por lo que puede hacer referencia a él. Esto también es lo que hace posible las llamadas recursivas.

  1. ¿Cómo wrapper cuenta el método antes de definir wrapper?

La pregunta también podría ser " ¿Cómo podría incrementar el wrapper.count antes de que se inicialice? "

Pero nuevamente, podemos responder esto de la misma manera: dado que los cuerpos de las funciones no se ejecutan hasta que los llamemos, wrapper.count se inicializa a 0 antes de wrapper.count += 1.

  1. ¿No debería ejecutarse la línea wrapper.count = 0 cada vez que llamo a foo ()?

Veamos lo que está pasando. Has escrito:

@counter
def foo():
  print('calling foo()')

Que es solo un azúcar sintáctico para esto:

foo = counter(foo)

Ahora, estamos llamando a la función counter con foo como argumento. ¿Qué hace counter?

def counter(func):
  def wrapper(*args, **kwargs):
    wrapper.count += 1
    # Call the function being decorated and return the result
    return wrapper.count
  wrapper.count = 0
  # Return the new decorated function
  return wrapper

En lenguaje humano,

  • define una función llamada wrapper que toma un número desconocido de argumentos posicionales y de palabras clave
  • asignar 0 como un atributo llamado count para la función wrapper
  • devolver wrapper a la persona que llama

Y cuando asignamos el resultado a la función foo, en realidad asignamos wrapper a foo. Entonces, cuando llamamos a foo, en realidad estamos llamando a wrapper. La línea wrapper.count = 0 está fuera de la función wrapper, por lo que no se ejecutará cada vez que llamemos a foo.

Por último, le recomiendo que vea charla de PyCon de Reuven M. Lerner sobre decoradores. .

Editar: no leí el cuerpo del contenedor, lo que demuestra que realmente no necesitas saber qué está dentro del contenedor. Mis explicaciones siguen siendo correctas. Pero, como se sugiere en la respuesta de @Mark Tolonen, su contenedor probablemente debería devolver func(*args,**kwargs) no wrapper.count

1
Asocia 25 jun. 2020 a las 17:12

El contenedor es incorrecto. return wrapper.count está mal. Como dice el comentario, debería devolver el resultado de la llamada a la función con los argumentos; de lo contrario, foo devolverá el número de veces que se llamó cada vez en lugar de su resultado real.

def counter(func):
    def wrapper(*args, **kwargs):    # "wrapper" function now exists
        wrapper.count += 1           # count doesn't exist yet, but will when wrapper is called.
        return func(*args,**kwargs)  # call the wrapped function and return result
    wrapper.count = 0                # init the attribute on the function
    return wrapper

# Every time "counter" is used, it defines a *different* wrapper function
# with its own localized "count" variable initialized to zero.
@counter
def foo(a,b):
  print(f'foo({a,b})')   # demonstrate that foo was called with the correct arguments.
  return a + b

@counter
def bar(a,b):
  print(f'bar({a,b})')   # demonstrate that bar was called with the correct arguments.
  return a * b

print(foo(1,2))
print(foo(b=3,a=4))     # keywords work, too.
print(bar(5,b=6))

print(f'foo() was called {foo.count} times.')
print(f'bar() was called {bar.count} times.')

Salida:

foo((1, 2))
3
foo((4, 3))
7
bar((5, 6))
30
foo() was called 2 times.
bar() was called 1 times.
1
Mark Tolonen 25 jun. 2020 a las 16:33

wrapper.count() es solo una variable en el espacio de nombres wrapper. Se define fuera de la función contenedora con wrapper.count = 0 y se ejecuta en el momento en que se decora la función. Por lo tanto, no es un método de wrapper (), sino una variable que es global a wrapper(). Cada vez que llama a foo(), se ejecuta la función wrapper(), es decir, aumenta el contador.

Puede reemplazar el comentario # Call the function ... por la llamada real a la función con func() para que muestre la salida de impresión de foo().

Los decoradores no son tan fáciles de entender. Aquí hay un enlace que probablemente lo ayudará a comprender qué está sucediendo exactamente: https: // realpython .com / primer-en-python-decoradores /

0
Ronald 25 jun. 2020 a las 14:10