Магические методы __copy__() и __deepcopy__()

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

Продолжаем тему копирования объектов с помощью функций copy() и deepcopy() и на этом занятии мы с вами рассмотрим два магических метода, позволяющих управлять процессом копирования объектов классов:

  • __copy__(self) – вызывается при использовании функции copy();
  • __deepcopy__(self, memodict={}) – вызывается при использовании функции deepcopy().

Чтобы лучше понять назначение этих методов, давайте рассмотрим их работу на конкретных примерах. Вначале мы воспользуемся классом Point, который определили на предыдущем занятии:

class Point:
    def __init__(self, *crds):
        self.crds = list(crds) if crds else []
 
    def __repr__(self):
        return f"{id(self)}: {self.crds}: {id(self.crds)}"

Как мы уже знаем, если к объекту:

pt = Point(1, 2, 3)

применить функцию copy():

pt_cp = copy.copy(pt)

то локальные атрибуты (список crds) будут общими для обоих объектов (pt и pt_cp).

Но что, если мы бы хотели изменить это стандартное поведение функции copy() именно для объектов класса Point  и сделать так, чтобы список crds также копировался? Для этого в классе Point можно прописать специальный магический метод __copy__(), который будет вызываться в момент применения функции copy() к объектам этого класса. Тогда в этом магическом методе можно реализовать любую желаемую логику. В нашем случае, получим:

class Point:
    def __init__(self, *crds):
        self.crds = list(crds) if crds else []
 
    def __copy__(self):
        return Point(*self.crds)
 
    def __repr__(self):
        return f"{id(self)}: {self.crds}: {id(self.crds)}"

Смотрите, мы здесь самостоятельно создаем новый объект Point с передачей в инициализатор набора координат. В результате создается новый полностью независимый объект этого класса, который, затем следует вернуть с помощью оператора return.

Давайте убедимся, что программа работает, как задумано. Выведем объекты в консоль:

print(pt)
print(pt_cp)

Увидим строчки:

2336088685520: [1, 2, 3]: 2336088685632
2336088736784: [1, 2, 3]: 2336088736640

Оба объекта pt и pt_cp имеют разные id списков, а значит, они совершенно независимы между собой.

Вот так, достаточно просто можно применять магический метод __copy__() для более тонкой настройки процесса копирования объектов того или иного класса.

Второй аналогичный метод __deepcopy__() позволяет реализовывать более сложные алгоритмы копирования различных иерархических структур. Как всегда, рассмотрим подробно его работу на конкретном примере.

Давайте предположим, что нам нужен класс, который бы описывал вершины и взаимосвязи произвольного графа, например, такого:

Для этого объявим класс с именем Node с двумя атрибутами: name – название вершины; links – список связей текущей вершины с другими аналогичными вершинами графа.

class Node:
    def __init__(self, name):
        self.name = name # название вершины графа
        self.links = [] # список связей текущей вершины с другими вершинами
 
    def connect(self, node):
        self.links.append(node)
 
    def __repr__(self):
        return f"Node {self.name}: self={id(self)}, links={id(self.links)}"

Создадим три объекта этого класса и соединим их так, как показано на рисунке. Получим:

a = Node('a')
b = Node('b')
c = Node('c')
 
a.connect(b)
b.connect(c)
c.connect(a)

Граф создан. Для его просмотра объявим следующую рекурсивную функцию:

def show_graph(node, path=set()):
    if node not in path:
        path.add(node)
        print(f"{node.name}: {node.links}")
 
        for obj in node.links:
            show_graph(obj, path)

В эту функцию достаточно передать любую вершину, чтобы отобразить всю связанную с ней цепочку. Например, команда:

show_graph(a)

выведет в консоль следующие строчки:

a: [Node b: self=1901532904208, links=1901532896320]
b: [Node c: self=1901533004496, links=1901532896192]
c: [Node a: self=1901532904080, links=1901532896384]

Видим полную конфигурацию созданного нами графа.

Следующим шагом, как вы уже догадались, мы бы хотели выполнить глубокое копирование такой структуры, то есть, всего графа целиком, а не только одну его отдельную вершину. Для этого достаточно воспользоваться командой:

graph = copy.deepcopy(a)

Переменная graph будет ссылаться на копию вершины ‘a’, а доступ к остальным вершинам можно было бы получить через связи между ними.

Однако если в процессе копирования вершин нужно вносить какие-либо дополнительные изменения, например, перед названиями вершин добавить слово copy, то для реализации этой логики нам потребуется в классе Node объявить магический метод __deepcopy__() следующим образом:

class Node:
    def __init__(self, name):
        self.name = name
        self.links = []
 
    def __deepcopy__(self, memodict={}):
        if self in memodict:
            return memodict.get(self)
 
        print(f"Deepcopy: {self.name}")
        node_cp = Node("copy " + self.name)
        memodict[self] = node_cp
        node_cp.links = [copy.deepcopy(obj, memodict) for obj in self.links]
 
        return node_cp
    ...

Смотрите, в этот метод дополнительно передается словарь memodict, содержащий ранее скопированные объекты, чтобы по рекурсии не копировать одни и те же вершины графа. Тем более, что он у нас циклический (замкнутый). Поэтому вначале идет проверка на наличие текущего копируемого объекта self в словаре memodict. Если он там находится, то возвращается его ранее созданная копия. Иначе на экран выводится сообщение о создании копии объекта с определенным именем и создается копия объекта self, как объект класса Node. Функцию deepcopy() здесь вызывать нельзя, иначе по рекурсии будет вызываться этот магический метод для одного и того же объекта. После создания копии вершины графа, в словарь memodict заносится запись с ключом self и значением node_cp. После этого необходимо сформировать копию списка связей с другими вершинами. Для этого в генераторе списка для каждого объекта из self.links вызывается функция deepcopy(). Это гарантирует, что новый объект будет корректно создан со всеми своими обновленными связями. В конце магический метод __deepcopy__() возвращает новый созданный объект node_cp.

Давайте убедимся, что этот алгоритм будет работать верно. Для этого после создания глубокой копии:

graph = copy.deepcopy(a)

вызовем функцию:

show_graph(graph)

В консоли увидим:

Deepcopy: a
Deepcopy: b
Deepcopy: c
copy a: [Node copy b: self=2731792081872, links=2731791899328]
copy b: [Node copy c: self=2731792082448, links=2731791899264]
copy c: [Node copy a: self=2731792012880, links=2731791899392]

Во-первых, вначале создаются все три вершины графа в момент вызова функции deepcopy(). И, во-вторых, переменная graph ведет на копию прежней вершины ‘a’ (это видно по разным id) и, далее, по связям links удается перебрать все остальные вершины, которые также имеют свои уникальные id, то есть, являются копиями вершин прежнего графа.

Вот пример того, как можно использовать магические методы __copy__() и __deepcopy__() для реализации своих собственных алгоритмов копирования объектов классов.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме