sqlalchemy mark-deleted 和 python 多继承下的方法解析顺序 MRO
sqlalchemy mark-deleted 和 python 多继承下的方法解析顺序 MRO
今天在弄一个 sqlalchemy 的数据库基类的时候,遇到了跟多继承相关的一个小问题,因此顺便看了一下 MRO
mark-deleted 在 sqlalchemy 中的实现
在做数据库的类时,由于重要的数据都不能直接删除,需要使用 mark-deleted 的方式,即在数据库中保留一个 deleted 的标记字段,根据这个标记来区分数据是否已被标记删除。被 mark-deleted 的数据,在普通查询时不能直接查询出来,但还需要支持对仅 mark-deleted 的查询,以及对所有数据的查询。不重要的数据不需要使用 mark-deleted 方式,可直接删除。
这些条件的限制经常会出现在数据库的设计要求。在 python 中这个问题相对好处理,model 的基类与 mark-deleted 的支持的类分开,mark-deleted 以 mixin 的方式,仅在需要支持 mark-deleted 的类中才使用。以下是使用 flask-sqlalchemy 的代码 ::
from myproj import db
...
class Base(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
...
def save(self):
db.session.add(self)
db.session.commit()
class SoftDeletedMixin(object):
deleted = db.Column(db.Boolean, default=False)
deleted_at = db.Column(db.DateTime)
...
def soft_delete(self):
self.deleted_at = datetime.datetime.utcnow()
self.deleted = True
self.save()
class User(...):
username = db.Column(db.String(64), nullable=False)
password_hash = db.Column(db.String(128))
...
在 flask-sqlalchemy 中,对 Model 的扩展里面有一个 query 属性,可方便支持查询操作。支持 mark-deleted 的类,默认的 query 时需要自带 filter 为 filter_by(deleted=False)
,因此,需要进一步对 SoftDeletedMixin 改造。查看 flask-sqlalchemy 的 Model ,可发现 query 使用了属性描述符 property descriptor,使用了 _QueryProperty
类来描述 query 属性,而在 _QueryProperty
中需要使用 query_class
作为 query 的基类。为了支持在 query 中使用 soft_delete
方法,同时增加默认过滤,我们需要扩展这个类,或者按照它的形式编写自己的描述符。
...
class _SoftDeletedQuery(BaseQuery):
def soft_delete(self, synchronize_session='evaluate'):
return self.update({'deleted': literal_column('id'),
'updated_at': literal_column('updated_at'),
'deleted_at': timeutil.utcnow()},
synchronize_session=synchronize_session)
class _SoftDeletedQueryProperty(_QueryProperty):
def __get__(self, instance, owner):
query = super(_SoftDeletedQueryProperty, self).__get__(instance, owner)
return query.filter_by(deleted=False)
class _SoftDeletedQueryAllProperty(_QueryProperty):
pass
class _SoftDeletedQueryDeletedProperty(_QueryProperty):
def __get__(self, instance, owner):
query = super(_SoftDeletedQueryProperty, self).__get__(instance, owner)
return query.filter_by(deleted=True)
然后,SoftDeletedMixin 修改为 ::
class SoftDeletedMixin(object):
...
query_class = _SoftDeletedQuery
query = _SoftDeletedQueryProperty(db)
query_all = _SoftDeletedQueryAllProperty(db)
query_deleted = _SoftDeletedQueryDeletedProperty(db)
接下来的一步,需要在 User 类中使用 SoftDeletedMixin ::
class User(SoftDeletedMixin, Base):
...
这里需要注意,由于 python 的多继承中对方法的搜索顺序是有约定的,不能交换 SoftDeletedMixin 和 Base 的顺序,否则就得不到正确的结果。
python MRO 和 C3 算法
多继承中方法解析顺序(Method Resolution Order,MRO),是支持多继承语言的语言必然要面对的问题,python 对 MRO 的处理在新式类(广度优先、从左到右)和经典类型(深度优先、从左到右)中的处理有所不同,见 https://www.python.org/download/releases/2.3/mro/。
根据文档的说法,从 python 2.3 起,mro 采用 C3 算法。
在 python 经典类多继承中,主要面对单调性和本地优先级的问题:
单调性(monotonicity):对于继承了若干基类 AB 的类 C ,如果 C 的解析顺序为 AB ,那么 C 的所有子类的解析顺序应为 AB;
本地优先级(local precedence ordering):对于继承了 A 的类 C,如果 C 重写了 A 的方法或属性,那么 C 的所有子类访问该方法或属性时,应该优先选 C 而不是 A。
对于经典类采用深度优先算法,在 python 2.7 上,下面的代码存在本地优先级问题 ::
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B,A):
pass
通过 inspect.getmro(D)
可看到 mro 顺序为 DCAB 。那么如果 B 中重写了 A 的某个方法,在 D 中则无法访问到,因此存在本地优先级的问题。
下面的代码存在单调性问题 ::
class A:
pass
class B:
pass
class C(A, B):
pass
class D(B, A):
pass
class E(C, D):
pass
对于类 E,如果以深度优先,无论 A B 那个在前,必然会导致 C D 的任意一个单调性无法满足。通过 inspect.getmro(D)
得到的 mro 顺序为 ECABD ,可见优先级也无法满足。
新式类的 C3 算法在处理多重继承时,采用的 merge list 方法,而原理上与拓扑排序类似 http://xymlife.com/2016/05/22/python_mro/,但拓扑排序的说法不是很精确(图没有左右节点之分)。
C3 算法中,mro 是对类 C 继承层次的线性化(继承层次结构变为平坦线性结构),它采用以下的形式定义:
类列表记为 C1C2...CN
,其头部为 C1 ,尾部为 C2...CN
列表的组合 C+C1C2...CN = CC1C2...CN
类 C 继承 B1,B2,...,BN
,记作 C(B1B2...BN)
类 C 的线性化(即 MRO )表示其继承结构,记作 L[C(B1B2...BN)]
,采用以下迭代的方法计算:
L[C(B1B2...BN)] = C + merge(L[B1]...L[BN], B1,B2...BN)
L[object] = object
其中,merge 的计算方法为:
规定:对于 merge 中的 MRO 列表,某个列表的头部不在其它列表的尾部,那么它一个好的头部;
(1)遍历 merge 中的 MRO 列表,取得一个好的头部,如果找不到则无法 merge 并抛出异常;
(2)把好的头部加到 C 的 MRO 列表 并从 merge 中的所有 MRO 列表中移除该元素;
(3)如果某个列表为空白,那么从 merge 中移除,如果 merge 中没有列表则退出并返回结果。
那么对于第一个代码,C3 算法的结果为 DBCA ;第二个代码,C3 算法在得出了 L[E]=ECD+merge(ABObject,BAObject)
后无法选头,此时会抛出异常为 ::
Traceback (most recent call last):
File "<pyshell#48>", line 1, in <module>
class E(C,D):
TypeError: Error when calling the metaclass bases
Cannot create a consistent method resolution
order (MRO) for bases A, B