ООП в приложении Tkinter / tkinter 12

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Структурирование данных с помощью класса

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

Каждый контакт будет содержать следующую информацию:

  • Имя и фамилию. Эти значения не могут быть пустыми;
  • Адрес электронной почты, например, alex123@example.com;
  • Номер телефона в формате (123) 4567890.

С этой абстракцией можно приступать к написанию кода для класса Contact.

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


def required(value, message):
if not value:
raise ValueError(message)
return value

def matches(value, regex, message):
if value and not regex.match(value):
raise ValueError(message)
return value

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


import re

class Contact(object):
email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+")
phone_regex = re.compile(r"\([0-9]{3}\)\s[0-9]{7}")

def __init__(self, last_name, first_name, email, phone):
self.last_name = last_name
self.first_name = first_name
self.email = email
self.phone = phone

Однако этого определения недостаточно чтобы инициировать валидацию каждого поля. Для этого нужно использовать декоратор @property. С его помощью можно будет получить доступ к внутренним атрибутам:


@property
def last_name(self):
return self._last_name

@last_name.setter
def last_name(self, value):
self._last_name = required(value, "Фамилия обязательна")

Та же техника применяется и к first_name, поскольку и это поле является обязательным. Атрибуты email и phone также используют этот подход и функцию matches с соответствующим регулярным выражением:


@property
def email(self):
return self._email

@email.setter
def email(self, value):
self._email = matches(value, self.email_regex, "Invalid email format")

Готовый код в — structuring_data.py, позже его нужно будет использовать.

Как уже говорилось ранее, property — это механизм, который запускает вызовы функции, получая доступ к атрибутам объекта.

В этом примере они получают доступ к внутренним атрибутам с нижним подчеркиванием в начале названия:


contact.first_name = "John" # Сохраняется "John" в contact._first_name
print(contact.first_name) # Читается "John" из contact._first_name
contact.last_name = "" # ValueError вызвано функцией проверки данных

Дескриптор property обычно используется с синтаксисом @decorated — важно не забывать использовать одно и то же имя для декорируемых функций:


@property
def last_name(self):
# ...

@last_name.setter
def last_name(self, value):
# ...

Полная реализация класса contact может показаться перегруженной, ведь каждый атрибут нужно присвоить в методе __init__ и задать для него соответствующие методы получения и установки значения (геттеры и сеттеры).

К счастью, существуют способы, которые позволяют уменьшить количество шаблонного кода. Функция namedtuple из стандартной библиотеки позволяет создавать простые подклассы кортежей с именованными полями:


from collections import namedtuple

Contact = namedtuple("Contact", ["last_name", "first_name",
"email", "phone"])

Однако все еще нужно добавить обходной путь реализации валидации полей. Для этого используется пакет attrs, доступный в Python Package Index.

Установить его можно с помощью командной строки и pip:

pip install attrs

После этого все свойства можно заменить на attr.ib. Это также позволяет задавать функцию обратного вызова validator, которая принимает экземпляр класса, атрибут, который будет изменен, и значение, которое нужно задать.

С минимумом изменений можно переписать класс Contact, уменьшив количество строк кода вдвое:


import re
import attr

def required(message):
def func(self, attr, val):
if not val: raise ValueError(message)
return func

def match(pattern, message):
regex = re.compile(pattern)
def func(self, attr, val):
if val and not regex.match(val):
raise ValueError(message)
return func

@attr.s
class Contact(object):
last_name = attr.ib(validator=required("Фамилия обязательна"))
first_name = attr.ib(validator=required("Имя обезательно"))
email = attr.ib(validator=match(r"[^@]+@[^@]+\.[^@]+",
"Ошибка в поле email"))
phone = attr.ib(validator=match(r"\([0-9]{3}\)\s[0-9]{7}",
"Ошибка в поле phone"))

С внешними зависимостями в коде нужно обращать внимание не только на преимущества в плане продуктивности, но также на документацию, лицензирование и поддержку.

Больше информации о пакете attrs можно найти на сайте https://www.atrs.org/en/stable/.

Создание виджетов для отображения информации

Сложно создавать крупные приложения, если весь код содержится в одном классе. Разбив графический интерфейс на отдельные классы, можно сделать структуру программы модульной и создавать виджеты для определенных задач.

Помимо пакета Tkinter импортируем класс Contact из прошлого примера:


import tkinter as tk
import tkinter.messagebox as mb

from with_attr import Contact

Нужно убедиться, что файл with_attr.py находится в той же папке. В противном случае инструкция import-from вернет ошибку ImportError.

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


class ContactList(tk.Frame):
def __init__(self, master, **kwargs):
super().__init__(master)
self.lb = tk.Listbox(self, **kwargs)
scroll = tk.Scrollbar(self, command=self.lb.yview)

self.lb.config(yscrollcommand=scroll.set)
scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.lb.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)

def insert(self, contact, index=tk.END):
text = "{}, {}".format(contact.last_name, contact.first_name)
self.lb.insert(index, text)

def delete(self, index):
self.lb.delete(index, index)

def update(self, contact, index):
self.delete(index)
self.insert(contact, index)

def bind_doble_click(self, callback):
handler = lambda _: callback(self.lb.curselection()[0])
self.lb.bind("", handler)

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


class ContactForm(tk.LabelFrame):
fields = ("Фамилия", "Имя", "Email", "Телефон")

def __init__(self, master, **kwargs):
super().__init__(master, text="Contact", padx=10, pady=10, **kwargs)
self.frame = tk.Frame(self)
self.entries = list(map(self.create_field, enumerate(self.fields)))
self.frame.pack()

def create_field(self, field):
position, text = field
label = tk.Label(self.frame, text=text)
entry = tk.Entry(self.frame, width=25)
label.grid(row=position, column=0, pady=5)
entry.grid(row=position, column=1, pady=5)
return entry

def load_details(self, contact):
values = (contact.last_name, contact.first_name,
contact.email, contact.phone)
for entry, value in zip(self.entries, values):
entry.delete(0, tk.END)
entry.insert(0, value)

def get_details(self):
values = [e.get() for e in self.entries]
try:
return Contact(*values)
except ValueError as e:
mb.showerror("Ошибка валидации", str(e), parent=self)

def clear(self):
for entry in self.entries:
entry.delete(0, tk.END)

Как работает создание виджета

Важная особенность класса ContactList то, что он предоставляет возможность добавить функцию обратного вызова для двойного клика. Он также передает индекс, по которому был осуществлен клик в качестве аргумента функции. Это нужно, чтобы скрыть детали реализации внутреннего класса Listbox:


def bind_doble_click(self, callback):
handler = lambda _: callback(self.lb.curselection()[0])
self.lb.bind("<Double-Button-1&rt;", handler)

У ContactForm также есть абстракция для создания экземпляра контакта на основе значений, введенных в Entry:


def get_details(self):
values = [e.get() for e in self.entries]
try:
return Contact(*values)
except ValueError as e:
mb.showerror("Ошибка валидации", str(e), parent=self)

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

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript