Python中的浅拷贝和深拷贝帮助
我觉得我在之前的问题中问得太复杂了,抱歉。这次我尽量简单明了地说明我的情况。
简单来说,我有一堆字典,这些字典里引用了我的对象,而这些对象是用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_ref
和allocated_rank
。students_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
字典。
所以我对students
和projects
都进行了深拷贝——我确实这样做了——他说我会遇到students
的allocated_proj_ref
属性与projects
字典中的实例匹配时出现问题。
因此,我想我必须在每个类中重新定义/重写(这就是它的意思吧?)deepcopy
,使用def __deepcopy__(self, memo):
或者类似的东西?
我想重写__deepcopy__
,使其忽略所有SQLA的东西(这些是<class 'sqlalchemy.util.symbol'>
和<class 'sqlalchemy.orm.state.InstanceState'>
),但复制映射类中的其他所有内容。
有什么建议吗?
2 个回答
这里有另一个选项,但我不确定它是否适合你的问题:
- 从数据库中获取对象以及所有需要的关联关系。你可以选择使用
lazy='joined'
或lazy='subquery'
来处理这些关系,或者调用查询的options(eagerload(relation_property)
方法,或者直接访问需要的属性来触发它们的加载。 - 将对象从会话中移除。此时,对象属性的懒加载将不再支持。
- 现在你可以安全地修改这个对象了。
- 当你需要将对象更新到数据库时,必须将它重新合并到会话中并提交。
更新:这里有一个概念验证的代码示例:
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',)]
如果我没记错的话,在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
对象的属性变化时,数据库不会受到影响。这意味着你可以安全地使用copy
或deepcopy
。当你完成所有操作后,可以将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
中。