Пример использования объектов property

Курс по Python ООП: https://stepik.org/a/116336

На этом занятии мы рассмотрим один практический пример использования объектов-свойств (property), с которыми познакомились на предыдущем занятии. Надеюсь, что из этого урока вы лучше поймете, зачем они нужны и как применяются на практике.

Давайте, как и на предыдущем занятии, объявим класс Person, который будет представлять данные о персонале некоторого учреждения. Предположим, что отдел кадров от нас требует хранить следующие персональные данные:

  • ФИО;
  • возраст (целое число от 14 до 120);
  • серию и номер паспорта в формате xxxx xxxxxx, где x – цифра (от 0 до 9);
  • вес, в кг (вещественное число от 20 и выше).

Первое, над чем задумывается программист, как описать эти данные на уровне программы. Я сделаю, следующим образом:

  • ФИО – список из трех строк: фамилия, имя, отчество;
  • возраст – целое число;
  • паспорт – строка в нужном формате;
  • вес – вещественное число.

Запишем этот класс со следующим инициализатором:

class Person:
    def __init__(self, fio, old, ps, weight):
        self.__fio = fio.split()
        self.__old = old
        self.__passport = ps
        self.__weight = weight

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

    @classmethod
    def verify_fio(cls, fio):
        if type(fio) != str:
            raise TypeError("ФИО должно быть строкой")
 
        f = fio.split()
        if len(f) != 3:
            raise TypeError("Неверный формат записи ФИО")
 
        letters = ascii_letters + cls.S_RUS + cls.S_RUS_UPPER
        for s in f:
            if len(s) < 1:
                raise TypeError("В ФИО должен быть хотя бы один символ")
            if len(s.strip(letters)) != 0:
                raise TypeError("В ФИО можно использовать только буквенные символы и дефис")

Дополнительно определю атрибуты S_RUS и S_RUS_UPPER в классе Person:

    S_RUS = 'абвгдеёжзийклмнопрстуфхцчшщьыъэюя-'
    S_RUS_UPPER = S_RUS.upper()

И импортируем коллекцию символов латинского алфавита:

from string import ascii_letters

Все, теперь, при передаче ФИО нужно через пробел записывать фамилию, имя и отчество. Причем, все они должны состоять только из буквенных символов или символа подчеркивания и иметь длину не менее одного символа. Иначе, мы генерируем исключение TypeError с соответствующим сообщением об ошибке.

Чтобы увидеть работу этого метода, пропишем его в инициализаторе:

    def __init__(self, fio, old, ps, weight):
        self.verify_fio(fio)
 
        self.__fio = fio.split()
        self.__old = old
        self.__passport = ps
        self.__weight = weight

Если проверки пройдут успешно, то исключений не возникнет и программа продолжит свою работу, иначе она будет завершена, либо обработано соответствующее исключение. Об обработке исключений мы с вами еще будем говорить.

Давайте попробуем создать объект с верной записью ФИО:

p = Person('Балакирев Сергей Михайлович', 30, '1234 567890', 80.0)

Как видим, никаких ошибок, но если указать что то неверное, то будут появляться ошибки и программа прервет свою работу. Это то, что я бы хотел в данном случае видеть.

Отлично, первый метод проверки мы с вами написали. Добавим еще три. Следующий будет проверять корректность ввода возраста:

    @classmethod
    def verify_old(cls, old):
        if type(old) != int or old < 14 or old > 120:
            raise TypeError("Возраст должен быть целым числом в диапазоне [14; 120]")

Следующий метод по аналогии будет проверять вес:

    @classmethod
    def verify_weight(cls, w):
        if type(w) != float or w < 20:
            raise TypeError("Вес должен быть вещественным числом от 20 и выше")

Наконец, последний метод для проверки корректности серии и номера паспорта:

    @classmethod
    def verify_ps(cls, ps):
        if type(ps) != str:
            raise TypeError("Паспорт должен быть строкой")
 
        s = ps.split()
        if len(s) != 2 or len(s[0]) != 4 or len(s[1]) != 6:
            raise TypeError("Неверный формат паспорта")
 
        for p in s:
            if not p.isdigit():
                raise TypeError("Серия и номер паспорта должны быть числами")

Осталось вызвать все эти методы в инициализаторе, чтобы они отрабатывали соответствующие проверки передаваемых данных:

    def __init__(self, fio, old, ps, weight):
        self.verify_fio(fio)
        self.verify_old(old)
        self.verify_ps(ps)
        self.verify_weight(weight)
 
        self.__fio = fio.split()
        self.__old = old
        self.__passport = ps
        self.__weight = weight

Как видите, все достаточно просто. Теперь, программист уверен, что в приватных свойствах экземпляров класса будут храниться корректные (с точки зрения формата) данные о пользователях.

Следующий шаг – это определить интерфейсы для взаимодействия с этими данными. Для этого воспользуемся объектами-свойствами, о которых говорили на предыдущем занятии.

Первое такое свойство будет для получения доступа к ФИО. Мы определим его через декоратор property, следующим образом:

    @property
    def fio(self):
        return self.__fio

В результате, у нас будет только геттер для получения ФИО сотрудника. Изменить эти данные через объект-свойство fio будет невозможно. Я решил так сделать в учебном примере, чтобы еще раз показать, что мы можем довольно гибко определять эти объекты.

Следующее свойство old будет использоваться и для считывания и для записи возраста:

    @property
    def old(self):
        return self.__old
 
    @old.setter
    def old(self, old):
        self.verify_old(old)
        self.__old = old

Обратите внимание, в сеттере мы также делаем проверку на корректность переданных данных. Проверку делаем с помощью ранее определенного метода verify_old.

Далее, опишем свойство для доступа к паспортным данным:

    @property
    def passport(self):
        return self.__passport
 
    @passport.setter
    def passport(self, ps):
        self.verify_ps(ps)
        self.__passport = ps

И последнее – для веса сотрудника:

    @property
    def weight(self):
        return self.__weight
 
    @weight.setter
    def weight(self, w):
        self.verify_weight(w)
        self.__weight = w

Все, мы описали базовый функционал для хранения указанных данных о сотрудниках с проверкой корректности передаваемых значений. Теперь мы можем воспользоваться этими объектами-свойствами, например, так:

p = Person('Балакирев Сергей Михайлович', 20, '1234 567890', 80.0)
p.old = 100
p.passport = '4567 123456'
p.weight = 70.0
print(p.__dict__)

В данном случае, мы передали корректные данные и они были записаны в соответствующие приватные локальные свойства объекта p.

Кроме того, можно непосредственно в инициализаторе воспользоваться объектами-свойствами, чтобы упростить текст программы:

    def __init__(self, fio, old, ps, weight):
        self.verify_fio(fio)
 
        self.__fio = fio.split()
        self.old = old
        self.passport = ps
        self.weight = weight

Мы здесь сразу через свойства осуществляем создание приватных локальных свойств в объекте и автоматически проверяем корректность переданных данных. Кроме ФИО, т.к. мы для него не определяли сеттер.

Я думаю, из примера этого занятия вы теперь стали лучше себе представлять, как работать с атрибутами класса и объектами-свойствами. На следующем занятии мы разовьем эту тему и поговорим о дескрипторах.

Курс по Python ООП: https://stepik.org/a/116336

Видео по теме