Python中的浅拷贝和深拷贝帮助

9 投票
2 回答
4361 浏览
提问于 2025-04-15 23:37

我觉得我在之前的问题中问得太复杂了,抱歉。这次我尽量简单明了地说明我的情况。

简单来说,我有一堆字典,这些字典里引用了我的对象,而这些对象是用SQLAlchemy映射的。这一切对我来说都没问题。不过,我想对这些字典的内容进行逐步修改。问题是,这样做会改变它们所引用的对象——而使用copy.copy()并没有帮助,因为它只会复制字典里的引用。因此,即使我复制了某些东西,当我尝试打印字典的内容时,我只会看到对象的最新更新值。

这就是我想使用copy.deepcopy()的原因,但这在SQLAlchemy中并不奏效。现在我陷入了困境,因为我需要在进行这些逐步修改之前,复制我对象的某些属性。

总之,我需要使用SQLAlchemy,同时确保在修改时能够保留对象属性的副本,这样就不会改变被引用的对象本身。

有没有什么建议、帮助或其他想法呢?


编辑:我添加了一些代码。

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_rankstudents_table是通过唯一的学生ID(sid)来索引的。


问题

我想保留我上面修改的属性——这基本上就是我决定使用SQLA的原因。然而,映射的对象会发生变化,这是不推荐的。因此,如果我对这个副本对象进行修改,未映射的对象……我能否将这些修改应用到映射对象的字段/表中。

从某种意义上说,我在遵循David的第二个解决方案,在这个方案中,我创建了一个未映射的类的另一个版本。


我尝试使用下面提到的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 必须被映射?


健康警告!

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

所以我对studentsprojects都进行了深拷贝——我确实这样做了——他说我会遇到studentsallocated_proj_ref属性与projects字典中的实例匹配时出现问题。

因此,我想我必须在每个类中重新定义/重写(这就是它的意思吧?)deepcopy,使用def __deepcopy__(self, memo):或者类似的东西?


我想重写__deepcopy__,使其忽略所有SQLA的东西(这些是<class 'sqlalchemy.util.symbol'><class 'sqlalchemy.orm.state.InstanceState'>),但复制映射类中的其他所有内容。

有什么建议吗?

2 个回答

2

这里有另一个选项,但我不确定它是否适合你的问题:

  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',)]
1

如果我没记错的话,在SQLAlchemy中,通常每次只会有一个对象对应于数据库中的某条记录。这是为了让SQLAlchemy能够保持你的Python对象和数据库之间的数据同步,反之亦然(当然,如果有其他程序同时在修改数据库,那就另当别论了)。所以问题在于,如果你复制了一个这样的映射对象,你就会得到两个不同的对象,它们都对应同一条数据库记录。如果你修改了其中一个,它们的值就会不同,而数据库无法同时匹配这两个对象。

我觉得你需要决定的是,当你修改复制对象的某个属性时,是否希望数据库记录也反映这些变化。如果希望,那就不应该复制对象,而是应该重用同一个实例。

另一方面,如果你不希望在更新复制对象时,原始的数据库记录发生变化,你还有另一个选择:这个复制对象应该成为数据库中的新行吗?还是根本不需要映射到数据库记录?如果是前者,你可以通过创建同一类的新实例并复制属性值来实现复制操作,基本上和你创建原始对象的方式一样。这通常会在你的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中。

撰写回答