复制字典及在SQLAlchemy ORM对象上使用深拷贝的问题

0 投票
2 回答
1175 浏览
提问于 2025-04-15 23:32

我正在做一个模拟退火算法,用来优化学生和项目的分配。

下面是来自维基百科的伪代码,不依赖于任何编程语言:

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 表示),并确保我始终保留之前的 nodebest_node

为了不修改我的原始字典(我可能想要重置它们),我做了字典的浅拷贝。从我读到的文档来看,这似乎只复制了引用,因为我的字典里包含对象,修改复制的字典最终也会改变对象。所以我尝试使用 copy.deepcopy()。这些字典引用的是用 SQLA 映射的对象。


问题:

我得到了关于遇到的问题的一些解决方案,但由于我对 Python 还很陌生,它们听起来都很复杂。

  1. 深拷贝和 SQLA 似乎不太兼容。我被告知在 ORM 对象上使用深拷贝可能会有问题,导致它无法按预期工作。显然,我最好是“构建拷贝构造函数,比如 def copy(self): return FooBar(....)。” 有人能解释一下这是什么意思吗?

  2. 我检查后发现 deepcopy 有问题,因为 SQLAlchemy 在你的对象上添加了额外的信息,比如一个 _sa_instance_state 属性,我不想在拷贝中保留这个,但这个属性对对象来说是必要的。我被告知:“有办法手动删除旧的 _sa_instance_state 并在对象上放一个新的,但最简单的方法是用 __init__() 创建一个新对象,并设置重要的属性,而不是做一个完整的深拷贝。” 这到底是什么意思?我是不是要创建一个新的、未映射的类,类似于旧的、已映射的类?

  3. 另一种解决方案是“在你的对象上实现 __deepcopy__(),并确保设置一个新的 _sa_instance_state,在 sqlalchemy.orm.attributes 中有一些函数可以帮助实现。” 这又超出了我的理解,能有人解释一下这是什么意思吗?

  4. 一个更一般的问题是:根据以上信息,有什么建议可以让我在我的 while 循环中保持 best_node(必须始终存在)和 previous_node 的信息/状态吗?也就是说,在解除分配/重新分配的过程中,不使用拷贝?

2 个回答

0

你不应该那样复制sqlalchemy对象。你可以自己写一些方法来轻松地复制它们,但这可能不是你想要的。你不想在数据库里有学生和项目的副本,对吧?所以不要复制那些数据。

你有一个字典来保存你的分配信息。在这个过程中,你绝对不要修改SQLAlchemy对象。所有可以修改的信息都应该保存在这些字典里。如果你需要修改对象以考虑这些信息,最后再把数据复制回去就行了。

2

我有另一个可能的解决办法:使用事务。这可能不是最好的方案,但实施起来应该会更快。

首先,像这样创建你的会话:

# transactional session
Session = sessionmaker(transactional=True)
sess = Session()

这样做的话,它就会是一个事务性的。事务的工作原理是,sess.commit() 会让你的更改变成永久的,而 sess.rollback() 则会把它们撤回。

在模拟退火的情况下,当你找到一个新的最佳解决方案时,你想要提交这个结果。之后的任何时候,你都可以调用 rollback() 来把状态恢复到那个时刻。

撰写回答