为什么此装饰程序会产生意外的输出?

时间:2018-11-08 04:28:56

标签: python python-3.x python-decorators

我编写了以下程序,以为两个函数price_reportsales_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 个答案:

答案 0 :(得分:3)

要确切地了解代码的内容要比需要困难,因为您在编写装饰器时选择了令人困惑的名称。这是一个与代码完全相同的版本,其名称已更改:

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,然后调用包装器,它返回{{1} }。这就是我上面所做的。

一旦您有了明智的名字,发现您有两个问题就容易多了:

  1. wrapper应该代替要修饰的函数,因此它应该具有相同的 signature -意味着它应该具有相同数量和类型的参数。您的函数wrapperprice_report完全不接受任何参数(即sales_report语句中括号()之间没有任何内容),但是def接受应该替换为参数的函数,这根本没有意义。

    该行应仅为wrapper,以匹配要替换的功能的签名。

  2. 装饰器应该返回替换函数,但是装饰器正在调用并返回结果。只需使用def wrapper():即可代替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)