*args 是向/​​从装饰函数传递参数的正确方法吗?

Posted

技术标签:

【中文标题】*args 是向/​​从装饰函数传递参数的正确方法吗?【英文标题】:is *args the proper way to pass parameters to/from decorated functions? 【发布时间】:2021-05-23 21:43:34 【问题描述】:

我有以下(简单的、家庭级的)问题:我将程序的状态保存在 JSON 文件中,并有几个使用该“数据库”的函数。有些只需要加载数据库,有些需要加载,然后写回文件。

我想在这些函数上使用装饰器来集中数据库的读写。下面是我的代码的简化版本,有两个功能:一个只使用数据库,另一个也修改它。此代码有效并返回预期值(*)

请注意数据库 (db) 是如何在装饰器和函数之间传递的

def load_db(func):
    def wrapper():
        print("loading DB")
        db = 5
        # the db was loaded and is now passed to the function actually making use of it
        func(db)
    return wrapper

def load_and_write_db(func):
    def wrapper():
        print("loading DB")
        db = 5
        # the db was loaded and is now passed to the function actually making use of it
        # we will get back the changed database
        db = func(db)
        # now we write the DB to the disk
        print(f"writing DB: db")
    return wrapper

@load_db
def do_stuff_load_only(*db):
    # a function that just consumes the DB, without changing it
    print(f"initial DB is db")

@load_and_write_db
def do_stuff_load_and_write(*db):
    # a function that consumes and chnages the DB (which then needs to be updated on disk)
    print(f"initial DB is db")
    db = 10
    print(f"changed DB to db")
    # returning the new DB
    return db


do_stuff_load_only()
do_stuff_load_and_write()

# Output:
# 
# loading DB
# initial DB is (5,)
# loading DB
# initial DB is (5,)
# changed DB to 10
# writing DB: 10

这是在装饰器和函数之间传递信息的正确方法吗?具体来说,我是否应该依靠*db 来指示只有装饰器将参数传递给函数,而在从代码中实际调用时什么都不传递(最后两行)?

This answer 很好地解释了应该如何传递参数,它只是未能解决我关于装饰函数有时接收参数的问题,有时没有。


(*)几乎db 传递给函数以元组的形式到达,我可以忍受

【问题讨论】:

不清楚你在问什么。标题询问使用* 将东西传递给装饰器,代码显示使用* 将东西传递给装饰函数。请注意,* 不直接与装饰器相关联,它表示可变参数 - 装饰器的参数通常是可变参数,但并非必须如此。 为什么load_db 定义一个忽略传递给修饰函数的参数并用硬编码值替换它的包装器? “我的问题是关于修饰函数有时接收参数,有时不接收参数”是什么意思??在我看来,您的装饰函数 always 收到了 db 参数,因为装饰器总是通过它。如果您想要一个可能作为参数传递也可能不作为参数传递的单个参数,给它一个默认值(即def foo(db=None): ...)比使用允许任意数量参数的*args 语法更明智(不只是 0 或 1)。 @kaya3: * 在我看来,您的装饰函数总是接收 db 参数,* - 请查看我的代码的最后两行,没有传递任何内容。现在,命名参数确实是一个更好的主意,没想到,谢谢! @WoJ 我已经看过你的代码了;请更仔细地阅读我的评论。仅仅因为您在没有传递参数的情况下调用(装饰的)函数,并不意味着该函数不接收参数;装饰者通过它。装饰器总是传递它,所以函数总是接收它。 【参考方案1】:

您的装饰器的编写方式,do_stuff_load_only 可以使用常规参数定义,即使您在调用它时实际上不会传递参数。这是因为名称 do_stuff_load_only 不会绑定到单参数函数;它将绑定到在load_db 中定义的zero-参数函数wrapperwrapper 本身会负责将参数传递给被修饰的实际单参数函数。

@load_db
def do_stuff_load_only(db):
    # a function that just consumes the DB, without changing it
    print(f"initial DB is db")


do_stuff_load_only()

定义do_stuff_load_only(*db)工作,但会改变实际绑定到db 的内容;在这种情况下,它将是单例元组 (5,) 而不是整数 5

如果您认为这看起来很尴尬,那是因为确实如此。 load_db 有一个“隐藏”的副作用,你不必担心。上下文管理器可能更适合这里:

from contextlib import contextmanager


@contextmanager
def load_db():
    print("Initializing the database")
    yield 5  # provide it
    print("Tearing down the database")


def do_stuff_load_only(*db):
    # a function that just consumes the DB, without changing it
    print(f"initial DB is db")


with load_db() as db:
    do_stuff_load_only(db)

现在do_stuff_load_only绑定的函数的定义和使用同意了,数据库的创建和销毁细节被上下文管理器隐藏了。上面代码的输出是

Initializing the database
initial DB is 5
Tearing down the database

【讨论】:

感谢您提供上下文管理器信息 - 这看起来很有希望。作为旁注,装饰器不应该是@contextmanager吗?对于“只读”版本,上下文管理器很简单,是否可以在“读取,然后写入”版本中使用它? 回答我自己的问题:可能是的,我正在检查处理 __enter____exit__ 的基于类的上下文管理器 我会先尝试使用contextmanager,因为它不那么冗长。基本上,yield 之前的任何内容都会进入结果 __enter__ 方法;产生的东西是__enter__ 返回的东西,yield 之后的所有东西都是__exit__ 方法。 contextmanager 只是为你生成方法。 (当然,除非您已经有一个现有的类,添加 __enter____exit__ 方法是有意义的。) 谢谢,这很清楚——我已经写了这个类,但我会重构它以使用contextmanager

以上是关于*args 是向/​​从装饰函数传递参数的正确方法吗?的主要内容,如果未能解决你的问题,请参考以下文章

*args and **kwargs

装饰器

从装饰器传递位置参数时如何支持静态和类方法?

闭包和装饰器

python基础 带参数以及返回值的函数装饰器

python@装饰符