【问题标题】:How to properly use a decorator? (TypeError: wrapper() takes 0 positional arguments but 1 was given)如何正确使用装饰器? (TypeError: wrapper() 接受 0 个位置参数,但给出了 1 个)
【发布时间】:2020-05-01 05:37:50
【问题描述】:

我正在尝试编写一个装饰器,在使用函数之前检查特定包是否可用。

在下面的示例中,numpy 不应引发错误,但 non_existent_test_package 应告知用户他们需要安装软件包才能使用此功能。这样做的目的是减少依赖。

根据@henry-harutyunyan 的建议更新


import numpy as np
import importlib

def check_available_packages(packages):
    if isinstance(packages,str):
        packages = [packages]
    packages = np.asarray(sorted(set(packages)))
    def wrapper(func):
        installed = list()
        for package in packages:
            try: 
                globals()[package] = importlib.import_module(package)
                installed.append(True)
            except ImportError:
                installed.append(False)
        installed = np.asarray(installed)
        assert np.all(installed), "Please install the following packages to use this functionality:\n{}".format(", ".join(map(lambda x: "'{}'".format(x), packages[~installed])))
        return func
    return wrapper

@check_available_packages(["numpy"])
def f():
    print("This worked")

@check_available_packages(["numpy", "non_existent_test_package"])
def f():
    print("This shouldn't work")

# ---------------------------------------------------------------------------
# AssertionError                            Traceback (most recent call last)
# <ipython-input-222-5e8224fb30bd> in <module>
#      23     print("This worked")
#      24 
# ---> 25 @check_available_packages(["numpy", "non_existent_test_package"])
#      26 def f():
#      27     print("This shouldn't work")

# <ipython-input-222-5e8224fb30bd> in wrapper(func)
#      15                 installed.append(False)
#      16         installed = np.asarray(installed)
# ---> 17         assert np.all(installed), "Please install the following packages to use this functionality:\n{}".format(", ".join(map(lambda x: "'{}'".format(x), packages[~installed])))
#      18         return func
#      19     return wrapper

# AssertionError: Please install the following packages to use this functionality:
# 'non_existent_test_package'

现在装饰器似乎在运行时检查包是否存在,而不是在实际调用函数时。如何调整此代码?

【问题讨论】:

  • check_available_packages 是一个装饰器 factory,它返回一个装饰器,该装饰器必须接受一个参数——要装饰的函数。参见例如stackoverflow.com/q/5929107/3001761

标签: python module package decorator python-decorators


【解决方案1】:

如果您希望在调用底层函数时进行检查,则需要对其进行额外的包装:

import functools

def check_available_packages(packages):
    if isinstance(packages,str):
        packages = [packages]
    packages = sorted(set(packages))
    def decorator(func):                # we need an extra layer of wrapping
        @functools.wraps(func)          # the wrapper replaces func in the global namespace
        def wrapper(*args, **kwargs):   # so it needs to accept any arguments that func does
            missing_packages = []       # no need for fancy numpy array indexing, a list will do
            for package in packages:
                try: 
                    globals()[package] = importlib.import_module(package)
                except ImportError:
                    missing_packages.append(package)
            assert not missing_packages, "Please install the following packages to use this functionality:\n{}".format(", ".join(missing_packages))
            return func(*args, **kwargs)  # call the function after doing the library checking!
        return wrapper
    return decorator

我从代码中删除了对numpy 的依赖,这对我来说似乎完全没有必要,特别是如果您正在测试是否安装了numpy,那么要求它进行检查是没有意义的.

【讨论】:

  • 谢谢!这比我以前干净得多。另外,感谢您解释正在发生的事情。我需要进一步研究 functools.wraps 。那么当我没有额外层 decorator 函数而只有 wrapper 时发生了什么?
  • 你不需要需要 functools.wraps,当你没有返回包装器来代替原始函数时,这很好。它用来自func 的属性替换了wrapper 函数的__name____doc__ 属性(可能还有一些其他的东西,我忘记了所有细节),因此更容易弄清楚该函数以后的作用上。没有它,装饰功能就可以正常工作。无论如何,如果没有额外的层,您必须在应用装饰器时进行库检查,因为稍后您无法拦截对该函数的调用。
【解决方案2】:

这会起作用

import numpy as np
import importlib


def check_available_packages(packages):
    if isinstance(packages, str):
        packages = [packages]
    packages = np.asarray(sorted(set(packages)))

    def decorator(func):
        def wrapper():
            installed = list()
            for package in packages:
                try:
                    globals()[package] = importlib.import_module(package)
                    installed.append(True)
                except ImportError:
                    installed.append(False)
            installed = np.asarray(installed)
            assert np.all(installed), "Please install the following packages to use this functionality:\n{}".format(
                ", ".join(packages[~installed]))
            func()

        return wrapper

    return decorator


@check_available_packages(["numpy"])
def foo():
    print("This worked")


@check_available_packages(["numpy", "non_existent_test_package"])
def bar():
    print("This shouldn't work")


foo()
bar()

问题是您拥有的 wrapper() 函数正在接受参数,而根据定义它不需要任何参数。所以在这个声明中传递_ wrapper(_) 就可以了。

_ 是虚拟的,不能使用,但它仍然是一个东西。 IDE 也不会抱怨未使用的变量。

要仅在调用函数时执行装饰器,您需要使用上面的装饰器工厂。详情请见this reference

【讨论】:

  • 谢谢。所以这是一种工作。我不希望 check_available_packages 进行评估,除非调用 f()。现在看起来它是在运行时而不是在调用函数时检查包。我怎样才能适应这个功能?
  • @O.rka 哦,我明白了,请将其添加到问题中,让我看看我们能做什么。
  • @O.rka 修改为仅在函数调用时执行。
猜你喜欢
  • 2020-05-05
  • 2020-06-13
  • 2017-07-23
  • 2013-09-23
  • 2017-10-05
  • 2016-01-30
  • 2019-06-26
  • 2014-11-12
  • 1970-01-01
相关资源
最近更新 更多