32. Неразрешимые трудности, дублирование информации, проблема

Продолжаем работать с двумерными списками (таблицами).
В прошлом уроке была создана программа для удобной работы с таблицей ячеек:

from random import randrange as rnd, choice
from tkinter import *
import itertools
 
root = Tk()
root.geometry('800x600')
 
canv = Canvas(root, bg = 'white')
canv.pack(fill = BOTH, expand = 1)


m = 34 # размер ячеек
d = 2 # размер поля вокруг ячейки
nr = 6 # количество строк
nc = 8 # количество столбцов
x0 = m // 2 # отступ от левого края
y0 = m // 2 # отступ от вернего края
colors = ['red','yellow','cyan','green']
 
class cell():
    def __init__(self, r, c): # при создании указываем номер строки и столбца, в который помещаем
        self.n = rnd(10) # значение, с которым будем работать
        self.r = r # Номер сторки в двумерном списке.
        self.c = c # Номер столбца ...
        self.color = choice(colors) # случайный цвет из списка
        self.id = canv.create_rectangle(-100,0,-100,0,fill = self.color) # квадратик ячейки
        self.id_text = canv.create_text(-100,0, text = self.n, font = "Arial " + str(m//2)) # текст в квадратике, размер зависит от масштаба
        self.paint()
 
    def paint(self):
        x1 = x0 + self.c * m + d
        # рассчитать координаты левого верхнего угла.
        y1 = y0 + self.r * m + d
        # координаты правого нижнего угла.
        x2 = x1 + m - 2*d # - r
        y2 = y1 + m - 2*d
        canv.coords(self.id,x1,y1,x2,y2)
        canv.itemconfig(self.id,fill = self.color)
        # текст в центр ячейки
        x = (x1 + x2) / 2
        y = (y1 + y2) / 2
        canv.coords(self.id_text,x,y)
        canv.itemconfig(self.id_text, text = self.n)
 
a = []
for r in range(nr): # 6 строк
    a.append([]) # создаем пустую строку
    for c in range(nc): # в каждой строке - 10 элементов
        a[r].append(cell(r,c)) # добавляем очередной элемент в строку
        
def task(event):
	pass
	
canv.bind('<3>', task)    

mainloop()

Рассмотрим некоторые задачи предыдущего урока.

Отметить всех соседей можно по-разному, например так:

def task(event):
    r = (event.y - y0) // m
    c = (event.x - x0) // m
    for dr,dc in [(0,0),(-1,0),(1,0),(0,-1),(0,1)]: # dr,dc - это смещение по строке и столбцу
        rr = dr + r
        cc = dc + c
        if rr in range(nr) and cc in range(nc): # Проверяем, является ли новый адрес допустимым
            a[rr][cc].color = 'orange'
            a[rr][cc].paint()

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

    for dr in [-1,0,1]:
        for dc in [-1,0,1]:

Обратите внимание, чтобы в обоих случаях мы берем и саму клетку. Не всегда это нужно делать. Исключим ее из перебора:

    for dr in [-1,0,1]:
        for dc in [-1,0,1]:
			if dr on dc:

if dr on dc даст ложь только если оба значения равны нулю, т.е. на самой клетке, а истину — если хотя бы одно значение не равно нулю, т.е. на всех соседях.
В первом случае, чтобы убрать саму клетку из перебора, достаточно удалить (0,0) из цикла.

А еще можно использовать модуль itertools. Выделение всех соседей (8 штук):

def mark1(rr,cc):
    for dr,dc in itertools.product([-1,0,1],[-1,0,1]):
            r = dr + rr
            c = dc + cc
            if r in range(nr) and c in range(nc):
                a[r].color = 'orange'
                a[r].paint()

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

def mark(rr,cc):
    for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]:
        r = dr + rr
        c = dc + cc
        if r in range(nr) and c in range(nc):
            a[r].color = 'orange'
            a[r].paint()

def task(event):
    for r in range(nr):
        for c in range(nc):
            a[r].color = 'gray'
            a[r].paint()
    rr = (event.y - y0) // m
    cc = (event.x - x0) // m
    for dr,dc in [(0,0),(-1,0),(1,0),(0,-1),(0,1)]:
        r = dr + rr
        c = dc + cc
        if r in range(nr) and c in range(nc):
            mark(r,c)

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

1. Удалить ячейку, по которой щелкнули

Добавим метод kill в класс cell

    def kill(self):
        canv.delete(self.id) # Удалить с экрана квадратик
        canv.delete(self.id_text) # Удалить с экрана текст
        a[self.r].remove(a[self.r][self.c]) # Удалить из списка ячейку

Удалим ячейку:

def task(event):
    r = (event.y - y0) // m
    c = (event.x - x0) // m
    a[r].kill()

Работает?
По-моему, не очень работает. Точнее работает, но не всегда.

2. В чем проблема? Когда не работает? Почему? Что с этим делать?

(Может помочь вывод значений ячеек в консоль. См. Справочник http://progras.ru/formatirovannyj-vyvod/

3. Удалить строку, по которой щелкнули

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

def task(event):
    r = (event.y - y0) // m
    #c = (event.x - x0) // m
    for c in range(nc-1,-1,-1):
        a[r].kill()

Почти работает.
«Почти», потому что кое-что остается. Посмотрите вывод в консоль и подумайте, что с этим можно сделать. Еще один маленький шаг.

4. Удалить столбец, по которому щелкнули
def task(event):
    #r = (event.y - y0) // m
    c = (event.x - x0) // m
    for r in range(nr):
        a[r].kill()

И опять почти работает. Природа ошибки та же, что и в случае с удалением ячейки.

Проблема серьезная.
Если мы попробуем поменять значение элементов местами, то опять будут сложности.

Причина в том, что мы храним одну и ту же информацию в ДВУХ местах. И при изменении в одном месте, не меняем в другом. Мы нарушили очень важный принцип НЕдублирования информации.

О чем это я?

Смотрите внимательно: номер строки и номер столбца каждой ячейки мы храним в свойствах ячейки. Но при этом, положение ячейки в таблице (двумерном списке) так же определяется номером строки и номером столбца. Пока мы не перемещали ячейки, эта проблема себя никак не проявляла. Когда же мы взялись удалять ячейки или перемещать их, то возник конфликт между r,c записанными в ячейку и r,c, характеризующими позицию этой ячейки в массиве. В результате мы получили ячейки, которые находятся в таблице в одном месте, а рисуют себя так, как-будто находятся в другом.

r,c в ячейке нам нужны для того, чтобы знать, где рисовать квадратик (изображение), не более того. Нужно найти другой способ риования ячейки и убрать эту информацию из класса cell.

Предлагаю немного подумать, прежде, чем переходить к следующему уроку.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *