Копирование списка делается неправильно

Рейтинг: 0Ответов: 1Опубликовано: 21.03.2023

Я решил ради потехи создать змейку в терминале python. И возникла проблема с отображением карты: есть список списков - поле размером width X height; я делаю копию списка в функции обновления карты и возвращаю обновлённую карту в главный цикл, где она и обрисовывается. Но почему-то переменная так называемой "чистой карты" переопределяется, хотя в коде этого нет.

Вот сам код:

import time
import os


class DevActions:
    def __init__(self):
        print("Инициализация действий разработчика . . .")

    @staticmethod
    def add_to_log(message: str, encoding: str = "utf-8"):
        """
        Записывает данные в лог файл. Построчно (экранирование работает [\n, \r, \b])
        :param message:  сообщение записываемое в лог
        :param encoding: кодировака
        :return:
        """
        with open(file = Settings.LOG_FILE, mode="a+", encoding=encoding) as log_file:
            log_file.write(message + "\n")


class Settings:
    EXTENSION_FOR_FILES = ".rqfile"
    LOG_FILE = "log" + EXTENSION_FOR_FILES


class Snake:
    def __init__(self):
        self.__size = 5
        self.direction_x = 1
        self.direction_y = 0
        self.__head = [9, 4]
        self.snake_skin = "@"
        self.speed_x = 1*self.direction_x
        self.speed_y = 1*self.direction_y
        self.__snake_cords = self.__create_snake()

    def __create_snake(self):
        snake_cords = []
        for i in range(self.get_size()):
            snake_cords.append([self.__head[0] - i, self.y_head()])
        return snake_cords

    def update(self):
        pixel = [self.x_head() + self.speed_x, self.y_head() + self.speed_y]
        self.__snake_cords.insert(0, pixel)
        self.__snake_cords.pop(len(self.__snake_cords) - 1)
        #print(self.__snake_cords)
        self.__head = pixel
        self.__redefine_directions()

    def __redefine_directions(self):
        """
        Переопределяет направления движения змейки.
        Если змека двигается вверх она не может двигаться вправо\лево
        И наоборот
        :return:
        """
        self.direction_x *= not self.direction_y
        self.direction_y *= not self.direction_x

    def change_direction(self, dir_x, dir_y) -> None:
        self.direction_y = dir_y
        self.direction_x = dir_x

    def x_head(self) -> int:
        return self.__head[0]

    def y_head(self) -> int:
        return self.__head[1]

    def get_head_cords(self) -> list[int, int]:
        return self.__head

    def get_size(self) -> int:
        return self.__size

    def get_snake(self) -> list:
        return self.__snake_cords


class Game:
    def __init__(self):
        self._dev = DevActions()
        self.color = "0"
        self.width = 32
        self.height = 10
        self.frame_update = None
        self.snake = Snake()
        self._origin_width_line = []
        self.iter_per_second = 10
        self.__iter_order = 0
        self.sleep_time = 1/self.iter_per_second
        self._map = []
        self.game = True
        self.pre_init()

    def pre_init(self):
        now_time = time.gmtime()
        time_str = f"////{now_time.tm_mday}-{now_time.tm_mon}, {now_time.tm_hour+5}:{now_time.tm_min}////"
        self._dev.add_to_log(time_str)

    def init_map(self):
        self._map = []
        for j in range(self.width):
            self._origin_width_line.append(self.color)
        for i in range(self.height):
            self._map.append(self._origin_width_line)

    def update_map(self):
        if self.snake.x_head() >= self.width or self.snake.y_head() >= self.height:
            self.death()
        map_cycle = None
        map_cycle = self._map.copy()
        for b in self._map:
            print("".join(b))
        print("Чистая карта вверху")

        print(map_cycle)

        for i in self.snake.get_snake():
            i: list
            y = i[1]
            x = i[0]
            map_cycle[y][x] = self.snake.snake_skin
        #print(f"{type(_map[y])}, {type(_map[y][x])}")  # отладка
        self.snake.update()
        return map_cycle

    def death(self):
        self.game = False
        os.system("cls")
        print("Вы проиграли")
        self._dev.add_to_log("////Конец игры////")
        time.sleep(0.5)
        exit()

    def __service_processing(self):  # Служебная обработка цикла
        self.__iter_order += 1
        if self.__iter_order == self.iter_per_second + 1:
            self.__iter_order = 1
        mess_1 = f"Итерация#{self.__iter_order} \n"
        mess_2 = f"snake_info:\n" \
                 f"size = {self.snake.get_size()}, \n" \
                 f"head_coord = ({self.snake.get_head_cords()})"
        self._dev.add_to_log(f"{mess_1} {mess_2}")

    def main_loop(self, game_loop=True):
        self.game = game_loop
        while self.game:
            u_map = self.update_map()
            for i in u_map:
                print("".join(i))
            # service_dev
            self.__service_processing()
            # -e
            time.sleep(self.sleep_time)
            os.system("cls")


if __name__ == '__main__':
    game = Game()
    game.init_map()
    game.main_loop()

Ошибка либо в копировании списка, либо я не знаю тонкостей. Смотрите функции update_map, main_loop

Ответы

▲ 3Принят

Я прочёл что вы уже решили проблему, но просили пояснений. Ваша проблема как вы верно подметили заключается в поверхностном копировании. Давайте разбираться почему так происходит, но для начала небольшая заметка [:](срезы), copy(), list() создают т.н. "поверхностные" копии. Начать стоит с того, что в python есть изменяемые и неизменяемые типы данных, слишком подробно на них останавливаться не буду, но как ясно из названия неизменяемые типы НЕ могут измениться в результате работы программы, а могут быть только переопределены:

num = 1
print(id(num))  # 140705244697632
num += 1
print(id(num))  # 140705244697664

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

list_one = [1, 2, 3, 4, 5]
print(id(list_one))  # 1864324240072
list_one.append(6)
print(id(list_one))  # 1864324240072

Отсюда, собственно, и возникает ваша ошибка, при копировании списка вы так же копируете ссылки на объекты, но не сами объекты, а значит при изменении объекта в одном месте это изменение затронет и все остальные места. Приведу упрощённый пример:

list_one = [1, 2, 3, 4, 5]
list_two = list_one
list_two.append(6)
print(list_one)  # [1, 2, 3, 4, 5, 6]
print(list_two)  # [1, 2, 3, 4, 5, 6]

Теперь к разнице между поверхностной копией и глубокой. Поверхностная копия создает новый объект, и затем вставляет в него ссылки на объекты, находящиеся в оригинале, а значит если вы измените что-либо в копии это отразиться и на оригинале:

list_one = [1, 2, [4, 5]]
list_two = list_one.copy()
list_two[2].append(6)
print(list_one)  # [1, 2, [4, 5, 6]]
print(list_two)  # [1, 2, [4, 5, 6]]

Что касается глубокой копии она создает новый составной объект, и затем рекурсивно вставляет в него копии объектов, находящихся в оригинале, что, по сути, создаёт новый объект с теми же значениями что имеет оригинальный список:

import copy

list_one = [1, 2, [4, 5]]
list_two = copy.deepcopy(list_one)
list_two[2].append(6)
print(list_one)  # [1, 2, [4, 5]]
print(list_two)  # [1, 2, [4, 5, 6]]