Геттеры и сеттеры в Python

Если вы работали с такими языками, как Java или C++, то, вероятно, привыкли писать геттеры и сеттеры для каждого атрибута в ваших классах. Эти методы позволяют получать доступ и изменять приватные атрибуты, сохраняя при этом инкапсуляцию.

В Python вы обычно раскрываете атрибуты как часть публичного API, а когда вам нужны атрибуты с функциональным поведением, используете свойства.

Несмотря на то, что использование свойств – это “питонично”, на практике вы можете столкнуться с недостатками такого подхода. Поэтому в некоторых ситуациях геттеры и сеттеры предпочтительнее свойств.

В этой статье мы рассмотрим:

  • как писать геттеры и сеттеры
  • замену геттеров и сеттеров свойствами
  • другие инструменты для замены геттеров и сеттеров
  • юзкейсы для применения геттеров и сеттеров

Чтобы извлечь максимальную пользу из этой статьи, вы должны быть знакомы с объектно-ориентированным программированием на языке Python. Также будет плюсом, если у вас есть базовые знания о свойствах и дескрипторах Python.

Знакомство с геттерами и сеттерами

Когда вы определяете класс в объектно-ориентированном программировании (ООП), у вас, скорее всего, будут какие-то атрибуты экземпляра и класса. Эти атрибуты – просто переменные, доступ к которым можно получить через экземпляр, класс или и то, и другое.

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

  • обращаться к атрибуту и изменять его напрямую
  • использовать методы.

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

Наличие атрибута, являющегося частью API класса, станет проблемой, если вам понадобится изменить внутреннюю реализацию самого атрибута.

Яркий пример этой проблемы – когда вы хотите превратить хранимый атрибут в вычисляемый. Хранимый атрибут будет немедленно реагировать на операции доступа и мутации, просто получая и сохраняя данные. А вычисляемый будет выполнять вычисления перед этими операциями.

Проблема обычных атрибутов заключается в том, что они не могут иметь внутренней реализации, так как это просто переменные. Поэтому изменение внутренней реализации атрибута потребует преобразования атрибута в метод. А это, скорее всего, приведет к поломке кода ваших пользователей. Почему? Чтобы их код продолжил работать, им придется преобразовать операции обращения к атрибутам и их изменения в вызовы методов во всей кодовой базе.

Чтобы справиться с этой проблемой, некоторые языки программирования, такие как Java и C++, требуют, чтобы вы предоставляли методы для манипулирования атрибутами ваших классов. Эти методы широко известны как геттеры и сеттеры (англ. getter и setter). Также вам могут встретиться названия accessor и mutator.

Что такое геттеры и сеттеры?

Геттеры и сеттеры довольно популярны во многих объектно-ориентированных языках программирования. Поэтому вполне вероятно, что вы уже слышали о них. В качестве грубого определения можно сказать, что:

  • геттер – это метод, позволяющий получить доступ к атрибуту в данном классе
  • сеттер – метод, позволяющий установить или изменить значение атрибута в классе

В ООП предполагается, что публичные атрибуты следует использовать только тогда, когда вы уверены, что никому и никогда не понадобится прикреплять к ним поведение. Если атрибут может изменить свою внутреннюю реализацию, то следует использовать геттеры и сеттеры.

Реализация паттерна геттеров и сеттеров требует:

  1. Сделать атрибуты непубличными
  2. Написать методы для получения доступа и изменения значения (т.е. геттеры и сеттеры) для каждого атрибута

Допустим, вам нужно написать класс Label с атрибутами text и font. Если вы намерены использовать геттеры и сеттеры для управления этими атрибутами, то класс вы напишете так:

# label.py

class Label:
    def __init__(self, text, font):
        self._text = text
        self._font = font

    def get_text(self):
        return self._text

    def set_text(self, value):
        self._text = value

    def get_font(self):
        return self._font

    def set_font(self, value):
        self._font = value

В этом примере конструктор Label принимает два аргумента, text и font. Эти аргументы хранятся в непубличных атрибутах экземпляра ._text и ._font соответственно.

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

Примечание: В Python нет понятия модификаторов доступа, таких как private, protected и public, для ограничения доступа к атрибутам и методам класса. В Python различают публичные и непубличные члены класса.

Если вы хотите показать, что данный атрибут или метод является непубличным, то вам следует использовать устоявшееся в Python правило префиксации имени с подчеркиванием (_).

Обратите внимание, что это всего лишь соглашение. Ничто не мешает вам и другим программистам обращаться к атрибутам, используя точечную нотацию, как в obj._attr. Однако нарушать это соглашение – плохая практика.

Вы можете использовать свой класс Label, как в примерах ниже:

>>> from label import Label

>>> label = Label("Fruits", "JetBrains Mono NL")
>>> label.get_text()
'Fruits'

>>> label.set_text("Vegetables")

>>> label.get_text()
'Vegetables'

>>> label.get_font()
'JetBrains Mono NL'

Label скрывает свои атрибуты от публичного доступа и вместо них раскрывает геттеры и сеттеры. Вы можете использовать эти методы, когда вам нужно получить доступ или изменить атрибуты класса, которые, как вы уже знаете, являются непубличными и поэтому не являются частью API класса.

Как появились геттеры и сеттеры?

Чтобы понять, откуда берутся геттеры и сеттеры, вернемся к примеру с Label. Допустим, вы хотите автоматически сохранять текст метки в верхнем регистре. К сожалению, нельзя просто добавить такое поведение к обычному атрибуту типа .text. Это можно сделать только через методы. Но преобразование публичного атрибута в метод приведет к ломающему изменению вашего API.

Итак, что же вы можете сделать? Ну, в Python вы, скорее всего, используете свойство (к этой теме мы еще вернемся). Однако такие языки программирования, как Java и C++, не поддерживают конструкции, подобные свойствам, или их свойства не совсем похожи на свойства Python.

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

Инкапсуляция – еще одна фундаментальная тема, связанная с происхождением геттеров и сеттеров. По сути, этот принцип означает объединение данных с методами, которые работают с этими данными. Таким образом, операции доступа и мутации выполняются исключительно через методы.

Этот принцип также связан с ограничением прямого доступа к атрибутам объекта. Такое ограничение позволяет избежать раскрытия деталей реализации или нарушения инвариантности состояния.

Чтобы обеспечить Label новой необходимой функциональностью в Java или C++, вы должны с самого начала использовать геттеры и сеттеры. Как можно применить этот паттерн для решения такой задачи в Python?

Рассмотрим следующую версию Label:

# label.py

class Label:
    def __init__(self, text, font):
        self.set_text(text)
        self.font = font

    def get_text(self):
        return self._text

    def set_text(self, value):
        self._text = value.upper()  # Добавленное поведение

В этой обновленной версии Label вы предоставляете геттеры и сеттеры для text. Атрибут, содержащий text, является непубличным, поскольку в его имени присутствует символ подчеркивания – ._text. Метод-сеттер (set_text(self, value)) преобразует введенный текст в заглавные буквы.

Теперь вы можете использовать свой класс Label, как в следующем фрагменте кода:

>>> from label import Label

>>> label = Label("Fruits", "JetBrains Mono NL")
>>> label.get_text()
'FRUITS'

>>> label.set_text("Vegetables")
>>> label.get_text()
'VEGETABLES'

Круто! Вы успешно добавили требуемое поведение атрибута text. Теперь у вашего метода-сеттера есть настоящая цель, а не просто присвоение нового значения целевому атрибуту. Его цель – добавить дополнительное поведение к атрибуту ._text.

Несмотря на то, что схема геттеров и сеттеров довольно распространена в других языках программирования, в Python это не так.

Добавление геттеров и сеттеров может значительно увеличить количество строк в вашем коде. Также эти методы следуют повторяющемуся и скучному шаблону, который потребует дополнительного времени на написание. К тому же, при его написании можно легко допустить ошибку. Вы также обнаружите, что функциональность, полученная от всего этого дополнительного кода, сразу зачастую равна нулю.

Ничто из этого не является пределом мечтаний питониста. В Python вы, вероятно, напишете класс Label, как в следующем фрагменте:

>>> class Label:
...     def __init__(self, text, font):
...         self.text = text
...         self.font = font
...

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

>>> label = Label("Fruits", "JetBrains Mono NL")
>>> label.text
'Fruits'

>>> # Later...
>>> label.text = "Vegetables"
>>> label.text
'Vegetables'

Делать атрибуты общедоступными, как в этом примере, – обычная практика в Python. В таких случаях переход на геттеры и сеттеры приведет к ломающим изменениям. Как же быть в ситуациях, когда требуется добавить поведение в атрибуты? Питоничный способ сделать это – заменить атрибуты свойствами.

Использование свойств вместо геттеров и сеттеров

Питоничный способ прикрепить поведение к атрибуту – это превратить сам атрибут в свойство. Свойства объединяют методы для получения, установки, удаления и документирования базовых данных. Таким образом, свойства – это специальные атрибуты с дополнительным поведением.

Вы можете использовать свойства так же, как и обычные атрибуты. Когда вы обращаетесь к свойству, автоматически вызывается его метод-геттер. Аналогично, когда вы изменяете свойство, вызывается его сеттер. Такое поведение позволяет подключать функциональность к атрибутам без внесения ломающих изменений в API вашего кода.

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

# employee.py

class Employee:
    def __init__(self, name, birth_date):
        self.name = name
        self.birth_date = birth_date

    # Implementation...

Конструктор этого класса принимает два аргумента – имя и дату рождения сотрудника. Эти атрибуты хранятся непосредственно в двух атрибутах экземпляра, .name и .birth_date.

Вы можете сразу же начать использовать этот класс:

>>> from employee import Employee

>>> john = Employee("John", "2001-02-07")

>>> john.name
'John'
>>> john.birth_date
'2001-02-07'

>>> john.name = "John Doe"
>>> john.name
'John Doe'

Employee позволяет создавать экземпляры, дающие прямой доступ к связанным с ними имени и дате рождения. Обратите внимание, что вы также можете изменять атрибуты с помощью прямых присваиваний.

По мере развития проекта у вас появляются новые требования. Вам необходимо хранить имя сотрудника в верхнем регистре и превращать дату рождения в date-объект.

Чтобы выполнить эти требования, не ломая API геттерами и сеттерами для .name и .birth_date, вы можете использовать свойства:

# employee.py

from datetime import date

class Employee:
    def __init__(self, name, birth_date):
        self.name = name
        self.birth_date = birth_date

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value.upper()

    @property
    def birth_date(self):
        return self._birth_date

    @birth_date.setter
    def birth_date(self, value):
        self._birth_date = date.fromisoformat(value)

В этой улучшенной версии Employee вы превращаете .name и .birth_date в свойства с помощью декоратора @property. Теперь у каждого атрибута есть геттер и сеттер с соответствующими именами.

Обратите внимание, что сеттер атрибута .name переводит вводимое имя в верхний регистр. Аналогично, сеттер .birth_date автоматически преобразует вводимую дату в date-объект.

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

>>> from employee import Employee

>>> john = Employee("John", "2001-02-07")

>>> john.name
'JOHN'

>>> john.birth_date
datetime.date(2001, 2, 7)

>>> john.name = "John Doe"
>>> john.name
'JOHN DOE'

Круто! Вы добавили поведение к атрибутам .name и .birth_date, не затрагивая API вашего класса. С помощью свойств вы получили возможность ссылаться на эти атрибуты так же, как и на обычные атрибуты. За кулисами Python позаботится о запуске соответствующих методов.

Не нужно нарушать код пользователя, внося изменения в API. Питоничный способ внести изменения – использовать декоратор @property.

Когда стоит использовать свойства?

Свойства официально рекомендованы в PEP 8 как правильный способ работы с атрибутами, требующими функционального поведения:

Для простых общедоступных атрибутов данных лучше всего раскрывать только имя атрибута, без сложных геттеров и сеттеров. Помните, что Python предоставляет легкий способ внести улучшения в будущем, если вы обнаружите, что простой атрибут данных нуждается в расширении функционального поведения. В этом случае используйте свойства, чтобы скрыть реализацию функционала за простым синтаксисом доступа к атрибуту данных. (Источник)

Свойства Python имеют множество потенциальных вариантов использования. Например, с помощью свойств можно элегантно и просто создавать атрибуты только для чтения, чтения-записи и записи. Свойства позволяют удалять и документировать базовые атрибуты и многое другое. Что еще более важно, при помощи свойств можно заставить обычные атрибуты вести себя как управляемые атрибуты с привязанным поведением, не меняя способа работы с ними.

В связи с использованием свойств питонисты стремятся проектировать API своих классов, придерживаясь нескольких рекомендаций:

  • Используйте публичные атрибуты, когда это уместно, даже если вы ожидаете, что атрибут потребует функционального поведения в будущем.
  • Избегайте определения сеттеров и геттеров для ваших атрибутов. При необходимости вы всегда можете превратить их в свойства.
  • Используйте свойства, когда вам нужно прикрепить поведение к атрибутам, и продолжайте использовать их в коде как обычные атрибуты.
  • Избегайте побочных эффектов в свойствах, потому что никто не ожидает, что операции вроде присваивания вызовут какие-либо побочные эффекты.

Свойства Python – это круто! Но из-за этого люди склонны злоупотреблять ими. В целом, свойства следует использовать только тогда, когда вам нужно добавить дополнительную обработку поверх определенного атрибута. Превращение всех ваших атрибутов в свойства будет пустой тратой времени. Это также может повлечь за собой проблемы с производительностью и поддерживаемостью.

Чем можно заменить геттеры и сеттеры в Python

Мы разобрали, как создавать простые геттеры и сеттеры для управления атрибутами ваших классов. Вы также узнали, что свойства – это питоничный способ добавления функционального поведения к существующим атрибутам.

В следующих разделах вы узнаете о других инструментах и техниках, которые можно использовать для замены геттеров и сеттеров в Python.

Дескрипторы Python

Дескрипторы – это продвинутый функционал Python, который позволяет создавать в классах атрибуты с привязанным поведением. Чтобы создать дескриптор, необходимо использовать протокол дескрипторов, в частности специальные методы .__get__() и .__set__().

Дескрипторы очень похожи на свойства. Фактически, свойство – это особый тип дескриптора. Однако обычные дескрипторы мощнее свойств и могут повторно использоваться в различных классах.

Давайте разберем пример использования дескрипторов для создания атрибутов с функциональным поведением. Предположим, что вам нужно продолжить разработку класса Employee. На этот раз вам нужен атрибут для хранения даты, когда сотрудник начал работать в компании:

# employee.py

from datetime import date

class Employee:
    def __init__(self, name, birth_date, start_date):
        self.name = name
        self.birth_date = birth_date
        self.start_date = start_date

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value.upper()

    @property
    def birth_date(self):
        return self._birth_date

    @birth_date.setter
    def birth_date(self, value):
        self._birth_date = date.fromisoformat(value)

    @property
    def start_date(self):
        return self._start_date

    @start_date.setter
    def start_date(self, value):
        self._start_date = date.fromisoformat(value)

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

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

# employee.py

from datetime import date

class Date:
    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        instance.__dict__[self._name] = date.fromisoformat(value)

class Employee:
    birth_date = Date()
    start_date = Date()

    def __init__(self, name, birth_date, start_date):
        self.name = name
        self.birth_date = birth_date
        self.start_date = start_date

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value.upper()

Этот код чище и менее повторяющийся, чем его предыдущая версия. В этом обновлении вы создаете дескриптор Date для управления атрибутами, связанными с датой. Дескриптор имеет метод .__set_name__(), который автоматически сохраняет имя атрибута. Он также имеет методы .__get__() и .__set__(), которые работают как геттер и сеттер атрибута соответственно.

Обе реализации Employee в этом разделе работают одинаково. Испытайте их сами!

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

Методы .__setattr__() и .__getattr__()

Еще один способ заменить традиционные геттеры и сеттеры в Python – использовать специальные методы .__setattr__() и .__getattr__() для управления атрибутами.

Рассмотрим следующий пример, в котором мы определяем класс Point. Класс автоматически преобразует входные координаты в числа с плавающей точкой:

# point.py

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __getattr__(self, name: str):
        return self.__dict__[f"_{name}"]

    def __setattr__(self, name, value):
        self.__dict__[f"_{name}"] = float(value)

Инициализатор Point принимает две координаты, x и y. Метод .__getattr__() возвращает координату, представленную name. Для этого метод использует словарь пространства имен экземпляра, .__dict__. Обратите внимание, что конечное имя атрибута будет содержать знак подчеркивания перед тем, что вы передадите в name. Python автоматически вызывает .__getattr__() всякий раз, когда вы обращаетесь к атрибуту Point, используя точечную нотацию.

Метод .__setattr__() добавляет или обновляет атрибуты. В этом примере .__setattr__() работает с каждой координатой и преобразует ее в число с плавающей точкой с помощью встроенной функции float(). Опять же, Python вызывает .__setattr__() всякий раз, когда вы выполняете операцию присваивания для любого атрибута класса.

Вот как этот класс работает на практике:

>>> from point import Point

>>> point = Point(21, 42)

>>> point.x
21.0
>>> point.y
42.0

>>> point.x = 84
>>> point.x
84.0

>>> dir(point)
['__class__', '__delattr__', ..., '_x', '_y']

Ваш класс Point автоматически преобразует значения координат в числа с плавающей точкой. Вы можете получить доступ к координатам x и y, как и к любому другому атрибуту. Но операции доступа и изменения проходят через .__getattr__() и .__setattr__() соответственно.

Обратите внимание, что Point позволяет вам обращаться к координатам как к общедоступным атрибутам. Однако он хранит их как непубличные. Вы можете убедиться в этом с помощью встроенной функции dir().

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

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

Когда использовать геттеры и сеттеры, а когда – свойства

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

Например, геттеры и сеттеры могут лучше подойти для ситуаций, в которых вам необходимо:

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

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

Медленные методы

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

Если вы слишком далеко отойдете от этих ожиданий, ваш API будет нарушать принцип наименьшего удивления. Он станет странным и неприятным в использовании.

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

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

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

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

Принятие дополнительных аргументов и флагов

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

Например, у вас есть класс Person с атрибутом .birth_date. Этот атрибут должен быть постоянным в течение жизни человека. Поэтому вы решили, что он будет доступен только для чтения.

Но нельзя сбрасывать со счетов человеческий фактор. Рано или поздно кто-то допустит ошибку при вводе даты рождения. Вы можете решить эту проблему, предоставив метод-сеттер, принимающий флаг force:

# person.py

class Person:
    def __init__(self, name, birth_date):
        self.name = name
        self._birth_date = birth_date

    def get_birth_date(self):
        return self._birth_date

    def set_birth_date(self, value, force=False):
        if force:
            self._birth_date = value
        else:
            raise AttributeError("can't set birth_date")

В этом примере вы предоставляете геттер и сеттер для атрибута .birth_date. Метод-сеттер принимает дополнительный аргумент force, который позволяет принудительно изменить дату рождения человека.

Примечание: Традиционные сеттеры обычно принимают не более одного аргумента. Приведенный выше пример может показаться странным или даже неправильным. Однако его цель – продемонстрировать технику, которая может быть полезна в некоторых ситуациях.

Вот как работает этот класс:

>>> from person import Person

>>> jane = Person("Jane Doe", "2000-11-29")
>>> jane.name
'Jane Doe'

>>> jane.get_birth_date()
'2000-11-29'

>>> jane.set_birth_date("2000-10-29")
Traceback (most recent call last):
    ...
AttributeError: can't set birth_date

>>> jane.set_birth_date("2000-10-29", force=True)
>>> jane.get_birth_date()
'2000-10-29'

Когда вы пытаетесь изменить дату рождения Джейн с помощью .set_birth_date() без установки force в True, вы получаете ошибку AttributeError, сигнализирующую о том, что атрибут не может быть установлен. А вот если вы установите значение force в True, то сможете обновить дату рождения Джейн, чтобы исправить все ошибки, допущенные при вводе даты.

Важно отметить, что свойства Python не принимают дополнительных аргументов в своих методах-сеттерах. Они просто принимают значение, которое нужно установить или обновить.

Использование наследования

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

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

В качестве примера рассмотрим следующую иерархию классов:

# person.py

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

class Employee(Person):
    @property
    def name(self):
        return super().name.upper()

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

>>> from person import Employee

>>> jane = Employee("Jane")

>>> jane.name
'JANE'

>>> jane.name = "Jane Doe"
Traceback (most recent call last):
    ...
AttributeError: can't set attribute 'name'

Теперь .name является свойством только для чтения, потому что метод-сеттер родительского класса не был унаследован, а был переопределен совершенно новым свойством. Вы же не хотите этого, не так ли?

Как можно решить эту проблему наследования? Если вы используете традиционные геттер и сеттер, то проблема не возникнет:

# person.py

class Person:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

class Employee(Person):
    def get_name(self):
        return super().get_name().upper()

Эта версия Person предоставляет независимые геттер и сеттер. Employee, являясь подклассом Person, переопределяет метод-геттер для атрибута name. Этот факт не влияет на метод-сеттер, который Employee успешно наследует от своего родительского класса Person.

Вот как работает эта новая версия Employee:

>>> from person import Employee

>>> jane = Employee("Jane")

>>> jane.get_name()
'JANE'

>>> jane.set_name("Jane Doe")
>>> jane.get_name()
'JANE DOE'

Теперь Employee полностью функционален. Переопределенный метод-геттер работает должным образом. Метод-сеттер также работает, поскольку он был успешно унаследован от Person.

Возбуждение исключений

В большинстве случаев вы не ожидаете, что оператор присваивания типа obj.attribute = value вызовет исключение. А вот от методов такое поведение вполне ожидаемо. В этом отношении традиционные геттеры и сеттеры оказываются более явными, чем свойства.

Например, site.url = "123" на первый взгляд не может вызвать исключение. Это выглядит как обычное присвоение атрибута и должно вести себя соответственно.

С другой стороны, site.set_url("123") выглядит как нечто, что может вызвать исключение. Например, ValueError, если входное значение не является допустимым URL для сайта. В этом примере метод-сеттер более явный. Он четко выражает возможное поведение кода.

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

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

Облегчение интеграции команды и миграции проекта

Предоставление геттеров и сеттеров является обычной практикой во многих языках программирования. Если вы работаете над проектом на Python с командой разработчиков, которые раньше работали на других языках, то вполне вероятно, что паттерн геттеров и сеттеров покажется им более знакомым, чем свойства Python.

В такой разнородной команде использование геттеров и сеттеров может облегчить вхождение новых разработчиков в коллектив.

Использование геттеров и сеттеров также может способствовать согласованности API. У вас будет API, основанный на вызовах методов, а не сочетающий вызовы методов с прямым доступом к атрибутам.

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

Во всех вышеперечисленных ситуациях следует рассмотреть возможность использования традиционных геттеров и сеттеров вместо свойств.

Заключение

Теперь вы знаете, что такое геттеры и сеттеры. Благодаря этим методам мы можем получать доступ к атрибутам и изменять их, избегая изменений в API. Но поскольку в Python есть такая фича, как свойства, геттеры и сеттеры используются не так широко, как в других языках.

Свойства позволяют добавлять поведение к атрибутам, точно так же избегая изменений в API. Но хотя свойства – это питоничный способ заменить традиционные геттеры и сеттеры, их применение на практике не всегда удобно. Материал из этой статьи должен помочь вам определиться, когда использовать свойства, а когда геттеры и сеттеры.

Перевод статьи Leodanis Pozo Ramos “Getters and Setters: Manage Attributes in Python”.

1 комментарий к “Геттеры и сеттеры в Python”

  1. Пингбэк: Методы в Python - pythonturbo

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *