Почему эта программа-декоратор выдает неожиданный вывод?

Я написал следующую программу, чтобы обеспечить обертки (декораторы) для двух функций price_report а также sales_report, Я только что назначил обертки для этих функций (последние две строки в коде ниже) без явного вызова price_report() или же sales_report(), Но код производит вывод, показанный ниже. Как так?

На самом деле, если я сделаю явный вызов price_report()Я получаю сообщение об ошибке TypeError: 'NoneType' object is not callable,

# wrapper.py

def wrapper(report):
    def head_and_foot(report):
        print(report.__name__)
        report()
        print("End of", report.__name__, "\n\n")
    return head_and_foot(report)

def price_report():
    cars  = ['Celerio', 'i10', 'Amaze', 'Figo']
    price = [500_000, 350_000, 800_000, 550_000]
    for x, y in zip(cars, price):
        print(f'{x:8s}', f'{y:8,d}')

def sales_report():
    cars  = ['Celerio', 'i10', 'Amaze', 'Figo']
    units = [5000, 3000, 1000, 800]
    for x, y in zip(cars, units):
        print(f'{x:8s}', f'{y:8,d}')

sales_report = wrapper(sales_report)
price_report = wrapper(price_report)

Вывод вышеуказанной программы (запускается ли она в блокноте Jupyter или из командной строки как python wrapper.py):

sales_report
Celerio     5,000
i10         3,000
Amaze       1,000
Figo          800
End of sales_report


price_report
Celerio   500,000
i10       350,000
Amaze     800,000
Figo      550,000
End of price_report

1 ответ

Решение

Сложнее, чем нужно, чтобы точно увидеть, что происходит с вашим кодом, потому что вы выбрали непонятные имена при написании вашего декоратора. Вот версия, которая делает то же самое, что и ваш код, с измененными именами:

def head_and_foot(func):
    def wrapper(func):
        print(func.__name__)
        func()
        print("End of", func.__name__, "\n\n")
    return wrapper(func)

def price_report():
    cars  = ['Celerio', 'i10', 'Amaze', 'Figo']
    price = [500_000, 350_000, 800_000, 550_000]
    for x, y in zip(cars, price):
        print(f'{x:8s}', f'{y:8,d}')

def sales_report():
    cars  = ['Celerio', 'i10', 'Amaze', 'Figo']
    units = [5000, 3000, 1000, 800]
    for x, y in zip(cars, units):
        print(f'{x:8s}', f'{y:8,d}')

sales_report = head_and_foot(sales_report)
price_report = head_and_foot(price_report)

Здесь есть три изменения:

  • wrapperhead_and_foot
  • head_and_footwrapper
  • reportfunc

Функция, которую вы вызвали wrapperкоторый я переименовал в head_and_foot, декоратор. Это означает, что она принимает функцию в качестве аргумента и возвращает другую функцию, которая предназначена для замены той, которую она приняла.

Обычно функция замены, которую она возвращает, является оберткой для исходной функции, что означает, что она делает то же самое, что и оригинальная функция, заключенная в некоторые дополнительные действия.

Для простоты принято называть декоратор именем, описывающим его эффект (например, head_and_footвызовите функцию, которую он принимает funcи вызвать обертку это возвращает wrapper, Это то, что я сделал выше.

Когда у вас появятся разумные имена, вам будет легче понять, что у вас есть две проблемы:

  1. wrapper предполагается, что он заменяет декорируемые функции, поэтому он должен иметь одинаковую сигнатуру, что означает, что он должен принимать одинаковое количество и тип аргументов. Ваши функции price_report а также sales_report вообще не принимайте никаких аргументов (т.е. между скобками нет ничего () в их def заявление), но wrapper принимает в качестве аргумента функцию, которую он должен заменить, что не имеет никакого смысла.

    Эта строка должна быть def wrapper(): соответствовать сигнатуре заменяемых функций.

  2. Предполагается, что декоратор возвращает функцию замены, но ваш декоратор вызывает замену и возвращает результат. Вместо return wrapper(func), тебе просто нужно return wrapper,

После внесения обоих этих изменений мы получаем следующее:

def head_and_foot(func):
    def wrapper():
        print(func.__name__)
        func()
        print("End of", func.__name__, "\n\n")
    return wrapper

def price_report():
    cars  = ['Celerio', 'i10', 'Amaze', 'Figo']
    price = [500_000, 350_000, 800_000, 550_000]
    for x, y in zip(cars, price):
        print(f'{x:8s}', f'{y:8,d}')

def sales_report():
    cars  = ['Celerio', 'i10', 'Amaze', 'Figo']
    units = [5000, 3000, 1000, 800]
    for x, y in zip(cars, units):
        print(f'{x:8s}', f'{y:8,d}')

sales_report = head_and_foot(sales_report)
price_report = head_and_foot(price_report)

Когда мы запускаем этот фиксированный код, мы не получаем неожиданного вывода, но мы получаем две функции, которые делают то, что мы ожидаем:

>>> price_report()
price_report
Celerio   500,000
i10       350,000
Amaze     800,000
Figo      550,000
End of price_report 


>>> sales_report()
sales_report
Celerio     5,000
i10         3,000
Amaze       1,000
Figo          800
End of sales_report 
Другие вопросы по тегам