ООП в приложении 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, если текст будет содержать невалидное значение. Для уведомления пользователя об этом выведем диалоговое окно с сообщением об ошибке.

Появились вопросы? Задайте на Яндекс Кью

У блога есть сообщество на Кью, подписывайтесь >> Python Q << и задавайте вопросы. Спрашивайте по контенту, про python и программирование в целом. Обещаю отвечать.

Вам помогла эта статья? Поделитесь в соцсетях или блоге. Репосты помогают сайту развиться.

Тест на знание python

Что выведет этот код?
Какая функция разворачивает список задом наперед?
Какой будет результат выполнения этого кода?
Что выведет этот код?
Какой цикл `for` выведет такой результат?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.