【问题标题】:Why does this decorator program produce an unexpected output?为什么这个装饰器程序会产生意想不到的输出?
【发布时间】:2019-04-11 14:19:49
【问题描述】:

我编写了以下程序来为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 notebook 中运行还是从命令行以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

【问题讨论】:

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


    【解决方案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 应该是被装饰的函数的替代品,所以它应该具有相同的 signature——这意味着它应该采用相同数量和类型的参数。您的函数price_reportsales_report 根本不接受任何参数(即在他们的def 语句中,括号() 之间没有任何内容),但wrapper 将它应该替换的函数作为参数,这根本没有意义。

      该行应该只是 def wrapper(): 以匹配被替换函数的签名。

    2. 装饰器应该返回替换函数,但您的装饰器调用替换并返回结果。你只需要return wrapper,而不是return wrapper(func)

    在进行了这两项更改之后,我们最终得到了这样的结果:

    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 
    

    【讨论】:

    • 我已经在书上标记了至少 10 篇关于装饰器的不同文章,但没有一篇提到关于命名约定的两点以及需要如此明确地返回什么。不只是两美分,而是两百万。我承认,当我构思和编写这段代码时,我的常识离开了法国。
    猜你喜欢
    • 2013-03-10
    • 1970-01-01
    • 2020-10-17
    • 2020-04-15
    • 1970-01-01
    • 2020-03-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多