$ sudo teach IT
МОДУЛЬ 5 · УРОК 2

Геттеры и сеттеры

Мы уже знаем, что атрибуты бывают публичными, защищёнными и приватными. Но как правильно давать к ним доступ снаружи — особенно когда нужна проверка данных? В этом уроке разберём геттеры и сеттеры, поймём их боль в «Java-стиле» — и почувствуем, зачем в Python придумали @property.

⏱ ~35 минут 🎓 Для новичков 🐍 Python 3

🟢 Прямой доступ — когда это нормально

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

Если у вас простой класс-контейнер данных, где нет никакой особой логики — просто используйте атрибуты напрямую. Вот пример:

class Point:
    """Точка на плоскости — простой контейнер координат."""
    def __init__(self, x, y):
        self.x = x
        self.y = y


p = Point(3, 7)
print(p.x)   # 3
print(p.y)   # 7

p.x = 10     # Изменяем напрямую — и это нормально!
p.y = -5
print(p.x, p.y)  # 10 -5

✅ Это питонично. Класс Point хранит координаты. Нет никаких ограничений на значения, нет сложной логики при изменении — поэтому прямой доступ уместен. Не нужно усложнять там, где это не нужно.

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

🤔 Когда прямого доступа недостаточно

Представьте, что вы пишете класс User с атрибутом age — возраст пользователя. Сначала кажется, что всё просто. Но потом вам ставят задачи:

🚫

Валидация

Возраст не может быть отрицательным или больше 150. Нужно проверять при записи.

🧮

Вычисляемые поля

«Полное имя» — это имя + фамилия. Его нет в базе, оно вычисляется на лету.

📋

Логирование

Нужно записывать в лог каждое изменение чувствительных данных — для аудита.

Прямой доступ user.age = -5 не даёт никакой возможности вмешаться и проверить значение. Нужен какой-то механизм, который позволяет «поймать» момент чтения или записи и выполнить свою логику. Именно для этого придумали геттеры (от get — «получить») и сеттеры (от set — «установить»).

☕ «Java-стиль»: get_name() и set_name()

В языке Java (и C++) есть чёткое правило: приватные атрибуты никогда не раскрываются напрямую. Для каждого из них пишут два специальных метода — геттер (для чтения) и сеттер (для записи). В Python это называют «Java-стилем» и иногда применяют.

Вот как это выглядит. Добавим валидацию возраста — он должен быть от 0 до 150:

class PersonJavaStyle:
    def __init__(self, name, age):
        self._name = name
        self._age = None        # Изначально None — установим через сеттер
        self.set_age(age)       # Сразу вызываем сеттер для валидации

    # --- Геттеры (get_...) ---
    def get_name(self):
        """Возвращает имя."""
        return self._name

    def get_age(self):
        """Возвращает возраст."""
        return self._age

    # --- Сеттеры (set_...) ---
    def set_name(self, value):
        """Устанавливает имя с проверкой."""
        if not isinstance(value, str) or len(value.strip()) == 0:
            raise ValueError("Имя должно быть непустой строкой")
        self._name = value.strip()

    def set_age(self, value):
        """Устанавливает возраст с валидацией."""
        if not isinstance(value, int):
            raise TypeError("Возраст должен быть целым числом")
        if value < 0 or value > 150:
            raise ValueError(f"Некорректный возраст: {value}. Допустимо от 0 до 150")
        self._age = value

Теперь посмотрим, как с этим классом работает пользователь:

# Создаём объект
user = PersonJavaStyle("Алиса", 25)

# Читаем данные — через геттеры
print(user.get_name())   # Алиса
print(user.get_age())    # 25

# Изменяем данные — через сеттеры
user.set_name("Боб")
user.set_age(30)
print(user.get_name())   # Боб
print(user.get_age())    # 30

# Валидация работает!
user.set_age(-5)         # ValueError: Некорректный возраст: -5. Допустимо от 0 до 150
user.set_age(200)        # ValueError: Некорректный возраст: 200. Допустимо от 0 до 150

⚠️ Валидация работает, но посмотрите на неудобство: вместо привычного user.age приходится писать user.get_age(). Вместо user.age = 30 — user.set_age(30). Код становится многословным. Но это ещё не главная проблема — главная впереди.

💥 Главная проблема: весь клиентский код нужно менять

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

Шаг 1. Сначала вы написали простой класс — без всякой валидации:

# Версия 1.0 — простой класс
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age   # Обычный публичный атрибут

Шаг 2. Ваш класс разошёлся по всему проекту. Коллеги используют его везде, прямым доступом:

# Файл orders.py
user = User("Алиса", 25)
print(user.age)   # Прямой доступ

# Файл profile.py
def show_profile(user):
    age = user.age    # Прямой доступ
    print(f"Возраст: {age}")

# Файл analytics.py
def get_stats(users):
    ages = [u.age for u in users]   # Прямой доступ в списковом включении
    return sum(ages) / len(ages)

# Файл admin.py
user.age = 30   # Прямое изменение

Шаг 3. Появляется задача: нужно добавить валидацию. Вы решаете сделать по Java-стилю — скрыть age и добавить get_age() / set_age():

# Версия 2.0 — добавили геттер и сеттер
class User:
    def __init__(self, name, age):
        self.name = name
        self._age = None
        self.set_age(age)

    def get_age(self):
        return self._age

    def set_age(self, value):
        if not isinstance(value, int) or value < 0 or value > 150:
            raise ValueError(f"Некорректный возраст: {value}")
        self._age = value

Шаг 4. Что теперь происходит со всем остальным кодом?

# Файл orders.py
user = User("Алиса", 25)
print(user.age)   # AttributeError! Атрибута 'age' больше нет!
                  # Нужно менять на: print(user.get_age())

# Файл profile.py
def show_profile(user):
    age = user.age    # AttributeError! Нужно: user.get_age()
    print(f"Возраст: {age}")

# Файл analytics.py
def get_stats(users):
    ages = [u.age for u in users]   # AttributeError! Нужно: u.get_age()
    return sum(ages) / len(ages)

# Файл admin.py
user.age = 30   # AttributeError! Нужно: user.set_age(30)

🔥 Вот она — главная проблема Java-стиля в Python:

Добавив валидацию через get_age()/set_age(), вы сломали весь существующий код, который использовал user.age. Теперь нужно вручную найти и исправить каждое упоминание user.age в сотнях файлов — заменить на user.get_age() и user.set_age(). В большом проекте это катастрофа.

Заметьте, что логически ничего не изменилось: возраст как был атрибутом объекта — так и остался. Вы просто добавили проверку. Но технически — интерфейс сломан.

💡 Идеальный сценарий — когда вы могли бы добавить логику при доступе к атрибуту, не меняя его внешнего вида. Код снаружи всё так же писал бы user.age, а внутри незаметно вызывалась бы функция с проверкой. Именно это решение называется @property — и мы изучим его в следующем уроке.

🚫 Почему get_age() / set_age() — не по-питонически

Python — это не Java. У них разная философия доступа к данным. Давайте сравним:

☕ Java-стиль

user.getName() — читать имя

user.setAge(25) — установить возраст

user.getFullName() — получить полное имя

Многословно. Нарушает «принцип наименьшего удивления» — выглядит не как данные, а как действие.

🐍 Python-стиль

user.name — читать имя

user.age = 25 — установить возраст

user.full_name — получить полное имя

Лаконично. Атрибуты выглядят как данные. За логику отвечает @property — незаметно для пользователя класса.

Функции с именами get_что-то() и set_что-то() — это «запах» чужого стиля в Python. Профессиональные Python-разработчики так не пишут. Если вам нужна логика при доступе — вы используете @property.

❌ Правило: Если вы пишете методы get_attr() и set_attr() вместо прямого доступа к атрибуту — скорее всего, вы делаете это неправильно. Исключение: когда метод делает что-то принципиально большее, чем просто «читает/записывает» — например, fetch_data_from_server() явно подчёркивает, что происходит сетевой запрос.

🤷 А когда геттеры-методы всё же уместны?

Есть ситуации, когда обычный метод с говорящим именем лучше, чем @property. Вот примеры:

class DatabaseUser:
    def __init__(self, user_id):
        self._user_id = user_id
        self._cached_orders = None

    # Этот метод явно говорит: "я иду в базу данных!"
    # Использовать @property здесь было бы обманом — атрибуты не должны стоить так дорого
    def fetch_orders(self):
        """Загружает заказы из базы данных."""
        print("Запрос к БД...")
        # ... здесь реальный запрос к БД ...
        return []

    # А вот это уместно сделать @property — быстро и без побочных эффектов
    @property
    def user_id(self):
        return self._user_id

💡 Правило большого пальца: если операция дорогая (сетевой запрос, обращение к диску, сложные вычисления) — используйте обычный метод, чтобы вызов был явным. Если операция быстрая и не имеет побочных эффектов — используйте @property (о нём — в следующем уроке).

🔄 Сравниваем: Java-стиль vs питонический стиль

Давайте поставим рядом оба варианта и посмотрим на разницу. Один и тот же класс — два подхода:

# ===== JAVA-СТИЛЬ (так в Python не пишут) =====
class PersonJavaStyle:
    def __init__(self, name, age):
        self._name = name
        self._age = None
        self.set_age(age)

    def get_name(self):
        return self._name

    def get_age(self):
        return self._age

    def set_name(self, value):
        if not isinstance(value, str) or not value.strip():
            raise ValueError("Имя должно быть непустой строкой")
        self._name = value.strip()

    def set_age(self, value):
        if not isinstance(value, int) or value < 0 or value > 150:
            raise ValueError(f"Некорректный возраст: {value}")
        self._age = value


# Как пользователь работает с Java-стилем:
user1 = PersonJavaStyle("Алиса", 25)
print(user1.get_name())     # Алиса   — некрасиво, похоже на действие
print(user1.get_age())      # 25
user1.set_age(26)           # некрасиво
print(user1.get_age())      # 26
# ===== ПИТОНИЧЕСКИЙ СТИЛЬ (будем изучать в следующем уроке) =====
class PersonPythonic:
    def __init__(self, name, age):
        self._name = name
        self._age = None
        self.age = age   # Вызовет setter через @property!

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

    @name.setter
    def name(self, value):
        if not isinstance(value, str) or not value.strip():
            raise ValueError("Имя должно быть непустой строкой")
        self._name = value.strip()

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0 or value > 150:
            raise ValueError(f"Некорректный возраст: {value}")
        self._age = value


# Как пользователь работает с питоническим стилем:
user2 = PersonPythonic("Боб", 30)
print(user2.name)   # Боб   — красиво, как настоящий атрибут
print(user2.age)    # 30
user2.age = 31      # красиво — выглядит как прямое присвоение
print(user2.age)    # 31

# Валидация ТАК ЖЕ работает!
user2.age = -1      # ValueError: Некорректный возраст: -1

🎯 Видите магию? Питонический стиль даёт то же самое — валидацию при записи — но внешний интерфейс остался прежним: user.age и user.age = 31. Если бы вы сначала написали класс без валидации, а потом добавили @property — никакой внешний код не сломался бы. Все, кто писал user.age, продолжают работать без изменений.

✅ Итоги урока

  • Прямой доступ (obj.attr) — норма в Python в 90% случаев. Не усложняйте без нужды.
  • Геттеры и сеттеры нужны, когда требуется валидация, вычисляемые поля или логирование доступа.
  • Java-стиль — это методы get_attr() / set_attr(). В Python он есть, но считается неидиоматичным.
  • Главная проблема Java-стиля: если изначально был user.age, а потом вы добавили геттер/сеттер — весь код, который использовал user.age, сломается. Придётся менять везде.
  • Методы get_что-то() оправданы только когда операция действительно «дорогая» — например, сетевой запрос. Это честный сигнал пользователю класса.
  • Питоническое решение — @property: добавляет логику при доступе к атрибуту, не ломая интерфейс.

👉 В следующем уроке мы изучим @property — элегантное решение всех проблем, которые мы разобрали сегодня. Вы поймёте, как Python позволяет «перехватывать» чтение и запись атрибута, сохраняя чистый синтаксис obj.attr. После этого урока Java-style геттеры и сеттеры вы не напишете никогда.