Помощь с копированием и глубокой копией в Python - PullRequest
9 голосов
/ 06 июня 2010

Я думаю, что пытался просить слишком много в моем предыдущем вопросе , поэтому извиняюсь за это. Позвольте мне изложить мою ситуацию настолько просто, насколько я могу на этот раз.

По сути, у меня есть несколько словарей, которые ссылаются на мои объекты, которые в свою очередь отображаются с использованием 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_ref и allocated_rank. students_table вводится с использованием уникального идентификатора студента (sid).


Question

Я бы хотел сохранить атрибуты, которые я изменил выше - я имею в виду, именно поэтому я решил использовать SQLA. Однако сопоставленный объект будет меняться, что не рекомендуется. Таким образом, если я внесу изменения в doppelgänger, unmapped object ... могу ли я принять эти изменения и обновить поля / таблицу для mapped object.

В некотором смысле я следую за вторичным решением Дэвида , где я создаю другую версию Класса, которая не отображается.


Я пытался использовать решение 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 делает копию everything . Мой allocated_proj_ref на самом деле является Project объектом, и у меня есть соответствующий projects словарь для этого.

Поэтому я делаю глубокие копии как students, так и projects - что я и есть - он говорит, что у меня будут случаи, когда у атрибута students allocated_proj_ref будут проблемы с соответствием экземплярам в projects словарь.

Таким образом, я понимаю, что мне придётся переопределить / переопределить (это так называется, не так ли?) deepcopy в каждом классе, используя def __deecopy__(self, memo): или что-то в этом роде?


Я бы хотел переопределить __deepcopy__, чтобы он игнорировал все компоненты SQLA (которые являются <class 'sqlalchemy.util.symbol'> и <class 'sqlalchemy.orm.state.InstanceState'>), но копировал все остальное, что является частью сопоставленного класса.

Есть предложения, пожалуйста?

Ответы [ 2 ]

2 голосов
/ 07 июня 2010

Вот еще один вариант, но я не уверен, что он применим к вашей проблеме:

  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 голос
/ 06 июня 2010

Если я правильно помню / думаю, в SQLAlchemy у вас обычно есть только один объект за раз, который соответствует данной записи базы данных. Это сделано для того, чтобы SQLAlchemy мог поддерживать ваши объекты Python в синхронизации с базой данных, и наоборот (хорошо, если нет одновременных мутаций БД извне Python, но это уже другая история). Таким образом, проблема в том, что если вы скопируете один из этих сопоставленных объектов, вы получите два разных объекта, которые соответствуют одной и той же записи базы данных. Если вы измените один из них, они будут иметь разные значения, и база данных не сможет сопоставить их обоих одновременно.

Я думаю, что вам может понадобиться решить, хотите ли вы, чтобы запись базы данных отражала изменения, которые вы вносите при изменении атрибута своей копии. Если это так, то вы вообще не должны копировать объекты, вы просто должны повторно использовать одни и те же экземпляры.

С другой стороны, если вы не хотите, чтобы исходная запись базы данных изменялась при обновлении копии, у вас есть другой выбор: должна ли копия стать новой строкой в ​​базе данных? Или это не должно быть сопоставлено с записью базы данных вообще? В первом случае вы можете реализовать операцию копирования, создав новый экземпляр того же класса и скопировав значения, почти так же, как вы создали исходный объект. Это, вероятно, будет сделано в методе __deepcopy__() вашего сопоставленного класса SQLAlchemy. В последнем случае (без сопоставления) вам потребуется отдельный класс, который имеет все те же поля, но не сопоставлен с использованием 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 extension 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...