复制字典及在SQLAlchemy ORM对象上使用深拷贝的问题
我正在做一个模拟退火算法,用来优化学生和项目的分配。
下面是来自维基百科的伪代码,不依赖于任何编程语言:
s ← s0; e ← E(s) // Initial state, energy.
sbest ← s; ebest ← e // Initial "best" solution
k ← 0 // Energy evaluation count.
while k < kmax and e > emax // While time left & not good enough:
snew ← neighbour(s) // Pick some neighbour.
enew ← E(snew) // Compute its energy.
if enew < ebest then // Is this a new best?
sbest ← snew; ebest ← enew // Save 'new neighbour' to 'best found'.
if P(e, enew, temp(k/kmax)) > random() then // Should we move to it?
s ← snew; e ← enew // Yes, change state.
k ← k + 1 // One more evaluation done
return sbest // Return the best solution found.
接下来是这个技术的一个改编。我导师说这个理论上是可以的。
首先,我从一堆随机分配中选一个分配(也就是一个包含学生及其分配项目的字典,还有项目的排名),复制它并传递给我的函数。我们把这个分配叫做 aOld
(它是一个字典)。aOld
有一个相关的权重,叫做 wOld
。权重的计算方法在下面描述。
这个函数的步骤如下:
- 把这个分配
aOld
设为best_node
- 从所有学生中随机挑选一些学生,放到一个列表里
- 把他们的项目去掉(也就是解除分配),同时更新项目(
allocated
参数现在变为False
)和讲师(如果他们的项目不再分配,释放出空位) - 把这个列表打乱顺序
- 再给这个列表里的每个人分配项目
- 计算这个新分配的权重(把排名加起来,排名1=1,排名2=2……没有项目的排名为101)
- 如果这个新分配
aNew
的权重wNew
小于一开始的权重wOld
,那么这个就是best_node
(根据上面的模拟退火算法定义)。对aNew
应用算法,继续进行。 - 如果
wOld < wNew
,那么对aOld
再次应用算法,继续进行。
这些分配/数据点被称为“节点”,所以一个 node = (weight, allocation_dict, projects_dict, lecturers_dict)
现在,我只能执行这个算法一次,但我需要尝试多次(在维基百科片段中用 kmax
表示),并确保我始终保留之前的 node
和 best_node
。
为了不修改我的原始字典(我可能想要重置它们),我做了字典的浅拷贝。从我读到的文档来看,这似乎只复制了引用,因为我的字典里包含对象,修改复制的字典最终也会改变对象。所以我尝试使用 copy.deepcopy()
。这些字典引用的是用 SQLA 映射的对象。
问题:
我得到了关于遇到的问题的一些解决方案,但由于我对 Python 还很陌生,它们听起来都很复杂。
深拷贝和 SQLA 似乎不太兼容。我被告知在 ORM 对象上使用深拷贝可能会有问题,导致它无法按预期工作。显然,我最好是“构建拷贝构造函数,比如 def copy(self): return FooBar(....)。” 有人能解释一下这是什么意思吗?
我检查后发现
deepcopy
有问题,因为 SQLAlchemy 在你的对象上添加了额外的信息,比如一个_sa_instance_state
属性,我不想在拷贝中保留这个,但这个属性对对象来说是必要的。我被告知:“有办法手动删除旧的_sa_instance_state
并在对象上放一个新的,但最简单的方法是用__init__()
创建一个新对象,并设置重要的属性,而不是做一个完整的深拷贝。” 这到底是什么意思?我是不是要创建一个新的、未映射的类,类似于旧的、已映射的类?另一种解决方案是“在你的对象上实现
__deepcopy__()
,并确保设置一个新的_sa_instance_state
,在 sqlalchemy.orm.attributes 中有一些函数可以帮助实现。” 这又超出了我的理解,能有人解释一下这是什么意思吗?一个更一般的问题是:根据以上信息,有什么建议可以让我在我的 while 循环中保持
best_node
(必须始终存在)和previous_node
的信息/状态吗?也就是说,在解除分配/重新分配的过程中,不使用拷贝?
2 个回答
你不应该那样复制sqlalchemy对象。你可以自己写一些方法来轻松地复制它们,但这可能不是你想要的。你不想在数据库里有学生和项目的副本,对吧?所以不要复制那些数据。
你有一个字典来保存你的分配信息。在这个过程中,你绝对不要修改SQLAlchemy对象。所有可以修改的信息都应该保存在这些字典里。如果你需要修改对象以考虑这些信息,最后再把数据复制回去就行了。
我有另一个可能的解决办法:使用事务。这可能不是最好的方案,但实施起来应该会更快。
首先,像这样创建你的会话:
# transactional session
Session = sessionmaker(transactional=True)
sess = Session()
这样做的话,它就会是一个事务性的。事务的工作原理是,sess.commit()
会让你的更改变成永久的,而 sess.rollback()
则会把它们撤回。
在模拟退火的情况下,当你找到一个新的最佳解决方案时,你想要提交这个结果。之后的任何时候,你都可以调用 rollback()
来把状态恢复到那个时刻。