使用装饰器的类内的函数包装器
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 上测试)。用户很可能不希望这样,他要么想在某个时候显式提交,要么在出现错误时完全避免提交。以上是关于使用装饰器的类内的函数包装器的主要内容,如果未能解决你的问题,请参考以下文章