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