使用装饰器的类内的函数包装器

Posted

技术标签:

【中文标题】使用装饰器的类内的函数包装器【英文标题】:Function wrappers inside class using decorators 【发布时间】:2018-10-16 02:11:59 【问题描述】:

我有一个与数据库交互的类,因此在该类的每个成员方法之前和之后都有重复的操作(建立会话、提交、关闭会话)。

如下:

class UserDatabaseManager(object):

    DEFAULT_DB_PATH = 'test.db'

    def __init__(self, dbpath=DEFAULT_DB_PATH):
        dbpath = 'sqlite:///' + dbpath
        self.engine = create_engine(dbpath, echo=True)

    def add_user(self, username, password):
        Session = sessionmaker(bind=self.engine)
        session = Session()
        # <============================== To be wrapped
        user = User(username, password)
        session.add(user)
        # ==============================>
        session.commit()
        session.close()

    def delete_user(self, user):
        Session = sessionmaker(bind=self.engine)
        session = Session()
        # <============================== To be wrapped
        # Delete user here
        # ==============================>
        session.commit()
        session.close()

用函数包装器抽象出重复会话调用的惯用方法是什么?

我更喜欢通过在 UserDatabaseManager 中声明一个私有 _Decorators 类并在其中实现包装函数来使用装饰器,但是这样的类将无法访问 self.engine 的实例属性外部类。

【问题讨论】:

@Luke 实际上,我对您的两个答案都投了赞成票,但目前很难说哪种方法更好,而且两者都非常相似。在做出决定之前,我一直想看看这两种方法如何在我的项目中扩展几天。但是非常感谢您回答我的问题! 【参考方案1】:

一个简单的(在我看来,也是最惯用的)方法是使用contextlib.contextmanager 将设置/拆卸样板代码包装在context manager 中。然后,您只需在执行该工作的函数中使用 with 语句(而不是尝试包装该函数本身)。

例如:

from contextlib import contextmanager

class UserDatabaseManager(object):

    DEFAULT_DB_PATH = 'test.db'

    def __init__(self, dbpath=DEFAULT_DB_PATH):
        dbpath = 'sqlite:///' + dbpath
        self.engine = create_engine(dbpath, echo=True)

    @contextmanager
    def session(self):
        try:
            Session = sessionmaker(bind=self.engine)
            session = Session()
            yield session
            session.commit()
        except:
            session.rollback()
        finally:
            session.close()

    def add_user(self, username, password):
        with self.session() as session:
            user = User(username, password)
            session.add(user)

    def delete_user(self, user):
        with self.session() as session:
            session.delete(user)

【讨论】:

我接受了这个答案,因为它更干净,但我必须把它交给 Ajax1234 的答案,因为它实际上用装饰器实现了相同的目的,并且在 dunder 方法周围有更多血腥的细节,这是一个接近的电话。 【参考方案2】:

你可以在类外创建一个简单的函数来包装每个方法:

def create_session(**kwargs):
   def outer(f):
     def wrapper(cls, *args):
       Session = sessionmaker(bind=getattr(cls, 'engine'))
       session = Session()
       getattr(session, kwargs.get('action', 'add'))(f(cls, *args))
       session.commit()
       session.close()
     return wrapper
   return outer

class UserDatabaseManager(object):
  DEFAULT_DB_PATH = 'test.db'
  def __init__(self, dbpath=DEFAULT_DB_PATH):
    dbpath = 'sqlite:///' + dbpath
    self.engine = create_engine(dbpath, echo=True)
  @create_session(action = 'add')
  def add_user(self, username, password):
    return User(username, password)

  @create_session(action = 'delete')
  def delete_user(self, user):
     return User(username, password)

一般来说,像上面这样的设置和拆卸操作最好放在上下文管理器中:

class UserDatabaseManager(object):
  DEFAULT_DB_PATH = 'test.db'
  def __init__(self, dbpath=DEFAULT_DB_PATH):
     dbpath = 'sqlite:///' + dbpath
     self.engine = create_engine(dbpath, echo=True)

class UserAction(UserDatabaseManager):
  def __init__(self, path):
    UserDatabaseManager.__init__(self, path)
  def __enter__(self):
    self.session = sessionmaker(bind=self.engine)()
    return self.session
  def __exit__(self, *args):
     self.session.commit()
     self.session.close()

with UserAction('/the/path') as action:
   action.add(User(username, password))

with UserAction('/the/path') as action:
   action.remove(User(username, password))

【讨论】:

提交一些语句有效但引发异常的会话不是一件坏事吗? @Rainy 你能详细说明一下吗?使用__enter____exit__,在设置或with 语句的主体中引发的任何异常都会导致当前程序崩溃。 抱歉这么久才回复;我的观点是with 正文中引发的异常将导致__exit__ 运行并提交会话。 (在 Python3.x 上测试)。用户很可能不希望这样,他要么想在某个时候显式提交,要么在出现错误时完全避免提交。

以上是关于使用装饰器的类内的函数包装器的主要内容,如果未能解决你的问题,请参考以下文章

如何使用装饰器将类的方法添加到类内的列表中

Python装饰器

装饰器和迭代器

我可以在包装函数之前修补 Python 装饰器吗?

装饰器的理解

dayfunctools.weps 定义函数装饰器