【问题标题】:Help with copy and deepcopy in Python帮助在 Python 中进行复制和深度复制
【发布时间】:2010-06-06 06:00:05
【问题描述】:

我想我试图在my previous question 中要求太多,所以对此表示歉意。这次让我尽可能简单地说明我的情况。

基本上,我有一堆引用我的对象的字典,而这些字典又使用 SQLAlchemy 进行映射。我一切都好。但是,我想对这些词典的内容进行迭代更改。问题是这样做会改变它们引用的对象——并且使用 copy.copy() 没有好处,因为它只复制字典中包含的引用。因此,即使复制了某些内容,当我尝试说print 字典的内容时,我也只会获得该对象的最新更新值。

这就是为什么我想使用 copy.deepcopy() 但它不适用于 SQLAlchemy。现在我处于两难境地,因为我需要在进行上述迭代更改之前复制对象的某些属性。

总之,我需要同时使用 SQLAlchemy ,确保在进行更改时我可以拥有我的对象属性的副本,这样我就不会更改引用的对象本身。

有什么建议、帮助、建议等吗?


Edit:添加了一些代码。

class Student(object):
    def __init__(self, sid, name, allocated_proj_ref, allocated_rank):
        self.sid = sid
        self.name = name
        self.allocated_proj_ref = None
        self.allocated_rank = None

students_table = Table('studs', metadata,
    Column('sid', Integer, primary_key=True),
    Column('name', String),
    Column('allocated_proj_ref', Integer, ForeignKey('projs.proj_id')),
    Column('allocated_rank', Integer)
)

mapper(Student, students_table, properties={'proj' : relation(Project)})

students = {}

students[sid] = Student(sid, name, allocated_project, allocated_rank)

因此,我将要更改的属性是 allocated_proj_refallocated_rank 属性。 students_table 使用唯一的学生 ID (sid) 键入。


Question

我想保留我在上面更改的属性——我的意思是,这基本上就是我决定使用 SQLA 的原因。但是,映射的对象会发生变化,不建议这样做。因此,如果我对 doppelgänger、unmapped 对象进行更改...我可以接受这些更改并更新 mapped 对象的字段/表吗?

从某种意义上说,我正在关注 David 的 secondary solution,在那里我创建了另一个未映射的 Class 版本。


我尝试使用下面提到的StudentDBRecord 解决方案,但出现错误!

File "Main.py", line 25, in <module>
    prefsTableFile = 'Database/prefs-table.txt')
File "/XXXX/DataReader.py", line 158, in readData
readProjectsFile(projectsFile)
File "/XXXX/DataReader.py", line 66, in readProjectsFile
supervisors[ee_id] = Supervisor(ee_id, name, original_quota, loading_limit)
File "<string>", line 4, in __init__
raise exc.UnmappedClassError(class_)
sqlalchemy.orm.exc.UnmappedClassError: Class 'ProjectParties.Student' is not mapped

这是否意味着Student 必须被映射?


Health warning!

有人在这里指出了一个非常好的附加问题。看,即使我在非映射对象上调用copy.deepcopy(),在这种情况下,假设它是我在上面定义的学生字典,deepcopy 会复制所有内容。我的allocated_proj_ref 实际上是一个Project 对象,我有一个对应的projects 字典。

所以我对studentsprojects 都进行了深度复制——我是——他说我会遇到studentsallocated_proj_ref 属性与@987654343 中的实例匹配存在问题的情况@字典。

因此,我认为我必须重新定义/覆盖(这就是所谓的不是吗?)deepcopy 在每个类中使用 def __deecopy__(self, memo): 或类似的东西?


我想覆盖 __deepcopy__ 以便它忽略所有 SQLA 内容(它们是 &lt;class 'sqlalchemy.util.symbol'&gt;&lt;class 'sqlalchemy.orm.state.InstanceState'&gt;),但复制属于映射类的所有其他内容。

有什么建议吗?

【问题讨论】:

  • 修改映射对象时,是保留原始版本还是修改后的版本?
  • @Winston:要么保留原件,要么保留特定的更改版本(best - 查看编辑)。我已经看到了后者的问题,因为best 并没有保持静止,因为我正在更新对象本身。叹息。
  • 响应您的编辑:您为什么要使用数据库来执行此操作?将Student 作为常规 Python 类,并仅在找到全局最优值之后将其存储在数据库(或文件)中会更有意义。
  • @Az: 是的,如果您要将信息存储在数据库中,您确实需要使用 SQLA 映射 something(或者当然您可以使用较低的级 DB 接口)。我的建议是你只写一次数据库,最后当你有最终的最佳解决方案时。将 SQLAlchemy 配置为在优化运行时忽略对 Student 实例的任何更改,或者使用常规未映射版本的 Student 进行算法,并且仅在完成后将数据传输到映射的 Student 对象中。
  • @Az:最后一点,没有理由必须是 __init__(self, student) 而不是 __init__(self, sid, ...)。无论哪种方式都会奏效。但是由于您只会将StudentDBRecord 构造为现有Student 的副本,因此将Student 传递给构造函数是有意义的。

标签: python copy sqlalchemy


【解决方案1】:

这是另一种选择,但我不确定它是否适用于您的问题:

  1. 从数据库中检索对象以及所有需要的关系。您可以将lazy='joined'lazy='subquery' 传递给关系,或者调用options(eagerload(relation_property) 查询方法,或者只访问所需的属性来触发它们的加载。
  2. 从会话中删除对象。从此时起,将不支持延迟加载对象属性。
  3. 现在您可以安全地修改对象了。
  4. 当您需要更新数据库中的对象时,您必须将其合并回会话并提交。

更新:这是概念验证代码示例:

from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relation, eagerload

metadata  = MetaData()
Base = declarative_base(metadata=metadata, name='Base')

class Project(Base):
    __tablename__ = 'projects'
    id = Column(Integer, primary_key=True)
    name = Column(String)


class Student(Base):
    __tablename__ = 'students'
    id = Column(Integer, primary_key=True)
    project_id = Column(ForeignKey(Project.id))
    project = relation(Project,
                       cascade='save-update, expunge, merge',
                       lazy='joined')

engine = create_engine('sqlite://', echo=True)
metadata.create_all(engine)
session = sessionmaker(bind=engine)()

proj = Project(name='a')
stud = Student(project=proj)
session.add(stud)
session.commit()
session.expunge_all()
assert session.query(Project.name).all()==[('a',)]

stud = session.query(Student).first()
# Use options() method if you didn't specify lazy for relations:
#stud = session.query(Student).options(eagerload(Student.project)).first()
session.expunge(stud)

assert stud not in session
assert stud.project not in session

stud.project.name = 'b'
session.commit() # Stores nothing
assert session.query(Project.name).all()==[('a',)]

stud = session.merge(stud)
session.commit()
assert session.query(Project.name).all()==[('b',)]

【讨论】:

  • 我对 SQLA 不是很有经验...能不能给我一个这个方法的更具体的例子?
  • 我现在可以结合使用映射和未映射的类。对我来说不太容易跟踪。但是,我会努力在将来尝试一下您的解决方案(并保留它以供参考)。感谢您的帮助:)
【解决方案2】:

如果我没记错的话,在 SQLAlchemy 中,通常一次只有一个对象对应于给定的数据库记录。这样做是为了让 SQLAlchemy 可以使您的 Python 对象与数据库保持同步,反之亦然(好吧,如果有来自 Python 外部的并发 DB 突变,那就是另一回事了)。所以问题是,如果你要复制这些映射对象之一,你最终会得到两个不同的对象,它们对应于相同的数据库记录。如果你换一个,那么它们会有不同的值,并且数据库不能同时匹配它们。

我认为您可能需要做的是决定是否希望数据库记录反映您在更改副本属性时所做的更改。如果是这样,那么您根本不应该复制对象,而应该重用相同的实例。

另一方面,如果您不希望在更新副本时更改原始数据库记录,您还有另一个选择:副本是否应该成为数据库中的新行?还是根本不应该映射到数据库记录?在前一种情况下,您可以通过创建同一类的新实例并复制值来实现复制操作,这与创建原始对象的方式几乎相同。这可能会在您的 SQLAlchemy 映射类的 __deepcopy__() 方法中完成。在后一种情况下(无映射),您将需要一个具有所有相同字段但未使用 SQLAlchemy 映射的单独类。实际上,让你的 SQLAlchemy 映射类成为这个非映射类的子类,并且只为子类做映射可能更有意义。

编辑:好的,澄清我最后一点的意思:现在你有一个Student 类用来代表你的学生。我的建议是让Student 成为一个未映射的常规类:

class Student(object):
    def __init__(self, sid, name, allocated_proj_ref, allocated_rank):
        self.sid = sid
        self.name = name
        self.allocated_project = None
        self.allocated_rank = None

并有一个子类,例如 StudentDBRecord,将映射到数据库。

class StudentDBRecord(Student):
    def __init__(self, student):
        super(StudentDBRecord, self).__init__(student.sid, student.name,
            student.allocated_proj_ref, student.allocated_rank)

# this call remains the same
students_table = Table('studs', metadata,
    Column('sid', Integer, primary_key=True),
    Column('name', String),
    Column('allocated_proj_ref', Integer, ForeignKey('projs.proj_id')),
    Column('allocated_rank', Integer)
)

# this changes
mapper(StudentDBRecord, students_table, properties={'proj' : relation(Project)})

现在您将使用未映射的Student 实例来实现您的优化算法——因此当Student 对象的属性发生变化时,数据库不会发生任何变化。这意味着您可以根据需要安全地使用copydeepcopy。完成后,您可以将 Student 实例更改为 StudentDBRecord 实例,类似于

students = ...dict with best solution...
student_records = [StudentDBRecord(s) for s in students.itervalues()]
session.commit()

这将创建与处于最佳状态的所有学生对应的映射对象,并将它们提交到数据库。

编辑 2:所以也许这行不通。一个快速的解决方法是将Student 构造函数复制到StudentDBRecord 并改为使StudentDBRecord 扩展object。也就是把之前StudentDBRecord的定义换成这样:

class StudentDBRecord(object):
    def __init__(self, student):
        self.sid = student.sid
        self.name = student.name
        self.allocated_project = student.allocated_project
        self.allocated_rank = student.allocated_rank

或者如果你想概括它:

class StudentDBRecord(object):
    def __init__(self, student):
        for attr in dir(student):
            if not attr.startswith('__'):
                setattr(self, attr, getattr(student, attr))

后一种定义会将Student 的所有非特殊属性复制到StudentDBRecord

【讨论】:

  • @David:刚刚用其中一个类的示例更新了我的问题。 “...如果您不希望在更新副本时更改原始数据库记录” - 这就是我正在寻找的。你能描述一下“you can implement the copy operation by creating a new instance of the same class and copying over the values”的确切含义吗?我是否为我的 SQLA 映射类定义了 __deepcopy__() 的“本地化”版本?我想我明白你最后一行的意思,但你能更具体地澄清一下吗?也许是一个例子?
  • 我的意思是类似于def __deepcopy__(self, memo): return Student(deepcopy(self.sid, memo), deepcopy(self.name, memo), deepcopy(self.allocated_project, memo), deepcopy(self.allocated_rank, memo))(请注意,如果这样做,您将无法再使用sid 作为主键)。
  • @David:已更新问题。 __deepcopy__() 方法对我来说有点太多了,因为我正在整理东西,但是,我有点遵循你的“单独的类-相同的字段-未映射”的解决方案。你能解释一下在这种情况下你所说的子类是什么意思吗?
  • @David:SQLA 可以处理继承吗?
  • @Az:当然,文档中甚至有一节介绍了如何映射在公共层次结构中相关的多个类。所以我不认为这样做会有任何麻烦,超类未映射而子类是。但我还没有实际测试过代码,所以我不能肯定地告诉你。
猜你喜欢
  • 1970-01-01
  • 2020-11-08
  • 2011-09-25
  • 2014-09-05
  • 2019-10-22
  • 2013-12-03
  • 2018-10-08
  • 1970-01-01
  • 2014-02-14
相关资源
最近更新 更多