Copy в программировании: перевод и копирование объектов в Python

Copy в программировании: перевод и копирование объектов в Python Полезное

Copy в IT переводится как «копировать» (глагол) или «копия» (существительное), а в программировании обычно означает создание нового объекта или файла с тем же содержимым, что у исходного. Главная ловушка для начинающих — в Python не всякая «копия» независима от оригинала. Ниже разберу перевод кратко, затем три разных действия: присваивание, поверхностную и глубокую копию, и в конце — копирование файлов модулем shutil. Примеры проверены на Python 3.14.

Как переводится copy

Форма Перевод Пример в IT
to copy (глагол) копировать, скопировать copy a file — скопировать файл
a copy (существительное) копия, экземпляр a copy of the list — копия списка
copy (жаргон радиосвязи) «принято», «понял» ответ в чате команды
copy (маркетинг) рекламный текст отсюда copywriting

Команда «Копировать» в интерфейсе — это Ctrl+C в Windows и Linux и Cmd+C в macOS, вставка — Ctrl+V / Cmd+V, вырезание — Ctrl+X / Cmd+X. Дальше речь только о копировании в коде.

Мини-словарь: три действия, которые путают

  • Присваивание (b = a) — копии нет, появляется второе имя того же объекта.
  • Поверхностная копия (shallow copy) — новый внешний объект, но вложенные объекты общие с оригиналом.
  • Глубокая копия (deep copy) — новый объект и рекурсивно новые копии всего изменяемого внутри.

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

Минимальный пример: все три варианта сразу

import copy

original = {"name": "Анна", "skills": ["python", "sql"]}

alias = original                  # присваивание: второе имя того же объекта
shallow = copy.copy(original)     # поверхностная копия
deep = copy.deepcopy(original)    # глубокая копия

original["name"] = "Мария"
original["skills"].append("git")

print(alias)
print(shallow)
print(deep)
print(alias is original, shallow is original, shallow["skills"] is original["skills"])

Результат запуска:

{'name': 'Мария', 'skills': ['python', 'sql', 'git']}
{'name': 'Анна', 'skills': ['python', 'sql', 'git']}
{'name': 'Анна', 'skills': ['python', 'sql']}
True False True

Разбор по строкам вывода:

  1. alias поменялся целиком — это тот же словарь, alias is original дает True.
  2. У shallow имя осталось «Анна»: ключ name в копии указывает на свою строку. Но список навыков общий, поэтому git появился и здесь.
  3. deep не изменился вообще — список skills в нем свой.

Аналогия: присваивание — второй ярлык на ту же папку; поверхностная копия — новая папка с ярлыками на те же документы; глубокая копия — новая папка с копиями документов.

Какой способ выбрать

Задача Что взять Когда иначе
Передать объект дальше без копии присваивание b = a если получатель будет менять данные, а оригинал нужен прежним
Плоский список или словарь (числа, строки) lst.copy(), lst[:], list(lst), dct.copy(), copy.copy() если внутри есть списки или словари, которые будут меняться
Вложенная изменяемая структура copy.deepcopy() большие структуры: deepcopy медленнее, иногда проще пересобрать данные явно
Объект с полями, нужна версия с другими значениями copy.replace() (Python 3.13+) учтите, что replace делает поверхностную копию

Все способы из второй строки дают поверхностную копию. Проверка на Python 3.14: nums.copy(), nums[:], list(nums) и copy.copy(nums) создают новый список, равный исходному и не совпадающий с ним по is.

Типичная ошибка: список списков через умножение

Частый случай «неявного копирования» — создание таблицы через *. Неверный код:

grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)

Фактический результат:

[[1, 0, 0], [1, 0, 0], [1, 0, 0]]

Умножение внешнего списка не копирует внутренний: три строки таблицы — это три ссылки на один и тот же список. Исправление — создавать каждую строку заново:

grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid)
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]

[0] * 3 внутри при этом безопасен: числа неизменяемы, общая ссылка на 0 ничего не ломает.

Неизменяемые объекты и циклические ссылки

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

import copy

t = (1, 2, 3)
print(copy.copy(t) is t, copy.deepcopy(t) is t)   # True True

t2 = (1, [2])
print(copy.deepcopy(t2) is t2)                    # False

node = {"name": "A"}
node["self"] = node
clone = copy.deepcopy(node)
print(clone["self"] is clone, clone is node)      # True False

Здесь две границы. Кортеж со списком внутри deepcopy все же копирует — иначе вложенный список остался бы общим. И deepcopy корректно обрабатывает циклы: он ведет словарь memo уже скопированных объектов, поэтому ссылка на себя в копии указывает на копию, а не на оригинал, и бесконечной рекурсии нет. Совпадение is у неизменяемых — деталь реализации, логику программы на нем не строят.

Когда deepcopy не справляется

Глубокая копия опирается на тот же протокол, что и pickle. Объекты с системными ресурсами — блокировки, открытые файлы, сокеты, соединения с базой — так не копируются:

import copy, threading

cfg = {"retries": 3, "lock": threading.Lock()}
try:
    copy.deepcopy(cfg)
except TypeError as e:
    print("TypeError:", e)
TypeError: cannot pickle '_thread.lock' object

Это правильное поведение: две «копии» одной блокировки или одного соединения не имеют смысла. Решение — копировать только данные, а ресурс создавать заново. В своем классе это делается методом __deepcopy__:

import copy

class Buffer:
    def __init__(self, data):
        self.data = data
        self.cache = {}          # кэш не копируем, строим заново

    def __deepcopy__(self, memo):
        clone = Buffer(copy.deepcopy(self.data, memo))
        return clone

b1 = Buffer([1, 2, 3])
b1.cache["sum"] = 6
b2 = copy.deepcopy(b1)
b2.data.append(4)
print(b1.data, b1.cache)   # [1, 2, 3] {'sum': 6}
print(b2.data, b2.cache)   # [1, 2, 3, 4] {}

memo нужно передавать дальше во вложенный deepcopy, иначе сломается обработка циклических ссылок. Для поверхностной копии аналогичный хук называется __copy__.

copy.replace: копия с изменениями

С Python 3.13 в модуле copy есть функция replace: она создает копию объекта с заменой указанных полей. Работает с dataclass, namedtuple и другими типами, которые поддерживают __replace__.

import copy
from dataclasses import dataclass, field

@dataclass
class Settings:
    host: str
    port: int
    tags: list = field(default_factory=list)

base = Settings("localhost", 8000, ["dev"])
prod = copy.replace(base, host="example.com", port=443)
prod.tags.append("prod")
print(prod)
print(base)
print(prod.tags is base.tags)
Settings(host='example.com', port=443, tags=['dev', 'prod'])
Settings(host='localhost', port=8000, tags=['dev', 'prod'])
True

Вывод показывает ту же ловушку: replace — поверхностная копия, список tags общий. Если вложенные поля будут меняться, передайте новое значение явно: copy.replace(base, tags=list(base.tags)).

Копирование файлов: shutil

Для файлов в Python есть модуль shutil. Функции отличаются тем, что переносят вместе с содержимым:

Функция Что копирует Аналог в командной строке
shutil.copyfile(src, dst) только содержимое, dst — путь к файлу прямого аналога нет, ближе всего cat src > dst
shutil.copy(src, dst) содержимое и права доступа; dst может быть папкой cp в Linux/macOS, copy в Windows
shutil.copy2(src, dst) то же плюс метаданные, в том числе время изменения cp -p
shutil.copytree(src, dst) папку рекурсивно (внутри по умолчанию copy2) cp -r, robocopy

Проверка разницы между copy и copy2 во временной папке:

import shutil, tempfile, os
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    src = Path(tmp) / "report.txt"
    src.write_text("итоги квартала\n", encoding="utf-8")
    os.utime(src, (1_700_000_000, 1_700_000_000))   # старое время изменения

    dst1 = shutil.copy(src, Path(tmp) / "copy.txt")
    dst2 = shutil.copy2(src, Path(tmp) / "copy2.txt")
    print(Path(dst1).stat().st_mtime == src.stat().st_mtime)   # False
    print(Path(dst2).stat().st_mtime == src.stat().st_mtime)   # True

На что обратить внимание при работе с файлами:

  1. shutil.copy и copy2 молча перезаписывают существующий файл назначения. Если это недопустимо, проверьте Path(dst).exists() перед копированием.
  2. copytree падает, если папка назначения уже есть: FileExistsError. Слить содержимое в существующую папку позволяет dirs_exist_ok=True (с Python 3.8), но совпадающие файлы при этом тоже перезапишутся.
  3. Символические ссылки copytree по умолчанию разыменовывает и копирует содержимое цели. Сохранить сами ссылки — параметр symlinks=True. Копируя папку из недоверенного источника, проверьте, куда ведут ссылки.
  4. Сохранение метаданных зависит от ОС и файловой системы: copy2 старается, но владелец файла и часть атрибутов могут не перенестись.

Выводы

  • Copy в IT — «копировать» или «копия»; в коде важно, насколько копия независима от оригинала.
  • b = a в Python не копирует объект, а дает ему второе имя.
  • Поверхностная копия (copy.copy, .copy(), срез) создает новый внешний объект, вложенные изменяемые объекты остаются общими; copy.replace тоже поверхностная.
  • copy.deepcopy копирует рекурсивно и справляется с циклами, но не копирует ресурсы вроде блокировок и соединений — для них пишут __deepcopy__.
  • Для файлов — shutil: copy2 сохраняет метаданные, copytree копирует папки, перезапись назначения происходит без предупреждения.

Где применяется / связь с практикой

Освойте тему на практике

Различие между ссылкой и копией встречается в Python постоянно: при передаче списков в функции, в значениях по умолчанию, в настройках приложения, при подготовке тестовых данных и обработке таблиц. Ошибки здесь не падают с исключением, а тихо портят данные, поэтому их лучше понимать заранее. Модель объектов, изменяемые и неизменяемые типы, работу с файлами и стандартной библиотекой последовательно разбирают на курсе Python Developer. Basic. Посмотреть формат занятий бесплатно можно на открытых уроках Otus.

FAQ

Чем list.copy() отличается от copy.copy(list)?
Для списка результат одинаковый — поверхностная копия. Метод .copy() есть у списков, словарей и множеств, а copy.copy() работает с любым объектом, поддерживающим протокол копирования.

Почему deepcopy медленный и можно ли без него?
Он обходит всю структуру и ведет словарь уже скопированных объектов. Если структура известна, быстрее пересобрать ее явно, например [row[:] for row in grid] для таблицы из списков чисел.

Копирует ли присваивание строку или число?
Нет, присваивание никогда не копирует. Но строки и числа неизменяемы, поэтому общая ссылка на них безопасна: любое «изменение» создает новый объект.

OTUS Журнал