Объектно-ориентированное программирование – очень удобный тип программирования, который позволяет работать с данными, как с объектами. Это позволяет с легкостью модифицировать определенные объекты кода, осуществлять откат, если возникают какие-то ошибки. Такие программы легко расширяются дополнительными функциями, если разработчик предусматривает такую возможность. И в целом, объектно-ориентированное программирование позволяет существенно упростить разработку программы.
Сегодня рассмотрим, как осуществляется работа с объектами в Tkinter. Ведь все мы понимаем, что каждый класс – это объект, верно?
Использование классов для структурирования данных
Очень просто демонстрировать использование классов в Python на примере приложения со списком контактов. Несмотря на то, что интерфейс будет отличаться, нам необходимо определить доменную модель. В нашем случае – каждый контакт.
Какая информация туда включается?
- Имя и фамилия. Очевидно, что здесь должно храниться какое-то значение.
- Адрес электронной почты. Допустим, у нас он следующий: alexx345@gmail.com
- Номер телефона. Он также указывается в определенном формате. Например, таком: (345) 6789012
Это абстракция – набор обобщенных характеристик, которые и отличают элемент определенного класса. В нашем случае таковым будет 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
Как видим из этого фрагмента кода, мы импортировали библиотеку re. Тем не менее, недостаточно лишь этого определения, чтобы была возможна проверка полей. Чтобы реализовать это, необходимо использовать декоратор @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")
Далее этот готовый код нужно будет использовать. Для этого необходимо сохранить его в отдельном файле, и потом на него ссылаться.
Как мы уже указывали ранее, с помощью механизма 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"])
Тем не менее, по-прежнему необходимо реализовать обходной путь проверки полей. Для этого используется Python Package Index, а именно, пакет attrs. Чтобы его инсталлировать, необходимо воспользоваться командной строкой, указав там команду pip и подкомманду install.
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 можно найти здесь.
Как создавать виджеты для отображения информации?
Чтобы создавать приложения было комфортно, необходимо разбивать его на отдельные классы. Тяжело сделать качественно приложение, если весь код предусмотрен лишь в одном классе. А вот при модульной организации приложения появляется возможность создавать отдельный элемент для каждой задачи.
В прошлом примере мы создали класс Contact. Его необходимо импортировать.
import tkinter as tk import tkinter.messagebox as mb from with_attr import Contact Необходимо удостовериться в том, что файл with_attr.py расположен в той же папке, что и этот код. В другом же случае эта инструкция выдаст ошибку 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)
Наш виджет предоставляет возможность добавить функцию обратного вызова для клика левой кнопкой мыши два раза. Также с его помощью можно получить индекс клика в качестве аргумента функции. Все это позволяет спрятать особенности внутреннего класса Listbox.
def bind_doble_click(self, callback): handler = lambda _: callback(self.lb.curselection()[0]) self.lb.bind("<Double-Button-1&rt;", handler)