Python 用可变数量的位置参数和可选参数装饰方法

Posted

技术标签:

【中文标题】Python 用可变数量的位置参数和可选参数装饰方法【英文标题】:Python decorate methods with variable number of positional args and optional arg 【发布时间】:2015-08-22 03:31:03 【问题描述】:

我正在使用 SQLalchemy 编写我的第一个 Python (3.4) 应用程序。我有几种方法都具有非常相似的模式。它们采用可选参数session,默认为None。如果session 被传递,该函数使用该会话,否则它打开并使用一个新会话。例如,考虑以下方法:

def _stocks(self, session=None):
    """Return a list of all stocks in database."""
    newsession = False
    if not session:
        newsession = True
        session = self.db.Session()
    stocks = [stock.ticker for stock in session.query(Stock).all()]
    if newsession:
        session.close()
    return stocks

所以,作为 Python 的新手并渴望了解它的所有功能,我认为这是学习一些有关 Python 装饰器的最佳时机。因此,经过大量阅读,像 this series of blog posts 和 this 这样美妙的答案,我写了以下装饰器:

from functools import wraps

def session_manager(func):
    """
    Manage creation of session for given function.

    If a session is passed to the decorated function, it is simply
    passed through, otherwise a new session is created.  Finally after
    execution of decorated function, the new session (if created) is
    closed/
    """
    @wraps(func)
    def inner(that, session=None, *args, **kwargs):
        newsession = False
        if not session:
            newsession = True
            session = that.db.Session()
        func(that, session, *args, **kwargs)
        if newsession:
            session.close()
        return func(that, session, *args, **kwargs)
    return inner

而且它似乎工作得很好。原来的方法现在简化为:

@session_manager
def _stocks(self, session=None):
    """Return a list of all stocks in database."""
    return [stock.ticker for stock in session.query(Stock).all()]

但是,当我将装饰器应用于除了可选的session 之外还接受一些位置参数的函数时,我得到一个错误。所以试着写:

@session_manager
def stock_exists(self, ticker, session=None):
    """
    Check for existence of stock in database.

    Args:
        ticker (str): Ticker symbol for a given company's stock.
        session (obj, optional): Database session to use.  If not
            provided, opens, uses and closes a new session.

    Returns:
        bool: True if stock is in database, False otherwise.
    """
    return bool(session.query(Stock)
                .filter_by(ticker=ticker)
                .count()
                )

print(client.manager.stock_exists('AAPL')) 一样运行会给出AttributeError,并带有以下回溯:

Traceback (most recent call last):
  File "C:\Code\development\Pynance\pynance.py", line 33, in <module>
    print(client.manager.stock_exists('GPX'))
  File "C:\Code\development\Pynance\pynance\decorators.py", line 24, in inner
    func(that, session, *args, **kwargs)
  File "C:\Code\development\Pynance\pynance\database\database.py", line 186, in stock_exists
    .count()
AttributeError: 'NoneType' object has no attribute 'query'
[Finished in 0.7s]

所以我通过回溯猜测,我弄乱了参数的顺序,但我不知道如何正确排序它们。除了session 之外,我还有想要装饰的函数可以接受0-3 个参数。有人可以指出我的方法中的错误吗?

【问题讨论】:

session 作为命名参数传递——func(stuff, session=session)。另外,为什么要调用func 两次?最后,这似乎真的应该有一个 db.session 的上下文管理器。 感谢@jwilner!两次调用func 只是我对语法的误解。我将func 调用更改为result = func(),然后返回结果。是的,关于db.session 上的上下文管理器也是正确的。我曾尝试删减一堆代码,以便更好地隔离我的问题。 【参考方案1】:

改变

def inner(that, session=None, *args, **kwargs):

def inner(that, *args, session=None, **kwargs):

return func(that, session, *args, **kwargs)

return func(that, *args, session=session, **kwargs)

有效:

def session_manager(func):

    def inner(that, *args, session=None, **kwargs):
        if not session:
            session = object()
        return func(that, *args, session=session, **kwargs)

    return inner


class A():

    @session_manager
    def _stocks(self, session=None):
        print(session)
        return True

    @session_manager
    def stock_exists(self, ticker, session=None):
        print(ticker, session)
        return True

a = A()
a._stocks()
a.stock_exists('ticker')

输出:

$ python3 test.py
<object object at 0x7f4197810070>
ticker <object object at 0x7f4197810070>

当您使用def inner(that, session=None, *args, **kwargs) 时,任何第二个位置参数(包括self)都被视为session 参数。所以当你调用 manager.stock_exists('AAPL') session 时,AAPL 会得到价值。

【讨论】:

谢谢,这真的很有帮助。我有一种感觉,我只是混淆了 args 的顺序。【参考方案2】:

我注意到的第一件事是你调用了两次装饰函数

@wraps(func)
    def inner(that, session=None, *args, **kwargs):
        newsession = False
        if not session:
            newsession = True
            session = that.db.Session()
        #calling first time
        func(that, session, *args, **kwargs)
        if newsession:
            session.close()
        #calling second time
        return func(that, session, *args, **kwargs)
    return inner

在第二次通话期间,会话已经关闭。 此外,您不需要在装饰器函数中显式接受thatsession 参数,它们已经在argskwargs 中。看看这个解决方案:

@wraps(func)
def inner(*args, **kwargs):
    session = None
    if not 'session' in kwargs:
        session = that.db.Session()
        kwargs['session'] = session
    result = func(*args, **kwargs)
    if session:
        session.close()
    return result
return inner

您可能还想将会话关闭代码放在finally 块中,那么即使装饰函数抛出异常,您也可以确保它被关闭

【讨论】:

没有在inner()func() 中明确传递that,我得到NameError: name 'that' is not defined

以上是关于Python 用可变数量的位置参数和可选参数装饰方法的主要内容,如果未能解决你的问题,请参考以下文章

python第五周,函数的定义与使用,代码复用与函数递归

python argparse详解

Python函数的参数类型

带有可选参数的 Postgresql 函数

Python入门-5函数:06参数类型-位置参数-默认值参数-命名参数-可变参数-强制命名参数

如何让 argparse 识别跟随可变长度可选参数的位置参数?