Свойства (property) и инкапсуляция в Python

Раздел: Основы Python -> ООП

Геттеры и сеттеры: основы и варианты реализации

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

Основное решение: декоратор @property

Самый простой и рекомендуемый способ - использовать встроенный декоратор @property для создания свойства, доступного как атрибут, но с методами-обработчиками.


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

    @property
    def name(self):
        """Геттер для имени"""
        return self._name

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

Get set python (геттеры и сеттеры в python)

Пояснение: атрибут _name считается защищённым (одно подчёркивание). Геттер name просто возвращает его значение. Сеттер name проверяет тип перед присваиванием. Пример использования:


p = Person("Алиса")
print(p.name)  # Алиса
p.name = "Боб"
print(p.name)  # Боб
# p.name = 123  # ValueError: Имя должно быть строкой

Python имя метода (имя метода в python)

Типичные ошибки:

  • Забыть поставить декоратор @property перед сеттером - сеттер не сработает и будет переопределён.
  • Использовать одинаковые имена для атрибута и свойства (например, self.name вместо self._name) - возникает бесконечная рекурсия.

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


Как создать свойство без использования декораторов?

Функция property() позволяет задать геттер, сеттер, удалитель и документацию в виде аргументов.


class Circle:
    def __init__(self, radius):
        self._radius = radius

    def get_radius(self):
        return self._radius

    def set_radius(self, value):
        if value < 0:
            raise ValueError("Радиус не может быть отрицательным")
        self._radius = value

    def del_radius(self):
        del self._radius

    radius = property(get_radius, set_radius, del_radius, "Радиус окружности")

Python print self (использование self в print python)

Порядок аргументов: fget, fset, fdel, doc. Если какой-то аргумент не нужен, передаётся None.


c = Circle(5)
print(c.radius)  # 5
c.radius = 10
print(c.radius)  # 10

метод remove python (метод remove в python)

Проблемы:

  • Легко перепутать порядок аргументов - вместо сеттера случайно передать удалитель.
  • Свойство, созданное этим способом, менее читаемо, чем вариант с декораторами.

Случаи использования: когда свойство создаётся динамически, например, в метапрограммировании.


Как переиспользовать логику геттеров и сеттеров для разных атрибутов?

Дескрипторы - это классы, реализующие протокол __get__ и/или __set__. Они позволяют вынести общую логику в отдельный класс.


class PositiveNumber:
    def __set_name__(self, owner, name):
        self.private_name = f"_{name}"

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("Значение должно быть положительным")
        setattr(instance, self.private_name, value)

class Product:
    price = PositiveNumber()
    quantity = PositiveNumber()

    def __init__(self, price, quantity):
        self.price = price
        self.quantity = quantity

какие есть методы в python (методы в python)

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


prod = Product(100, 5)
print(prod.price)  # 100
prod.price = 200
print(prod.price)  # 200
# prod.price = -1  # ValueError: Значение должно быть положительным

метод в программе python (методы в программе на python)

Проблемы:

  • Необходимость использовать __set_name__ для привязки имени атрибута.
  • Дескрипторы сложнее для понимания начинающими.

Цели: повторное использование кода валидации, создание доменно-специфичных типов атрибутов (например, Email, Phone).


Как перехватывать любые обращения к атрибутам?

Магические методы __getattr__ и __setattr__ позволяют контролировать доступ ко всем атрибутам объекта, что полезно для прокси-объектов или динамических хранилищ.


class SmartDict:
    def __init__(self):
        self._data = {}

    def __getattr__(self, name):
        if name.startswith('_'):  # защищённые атрибуты не трогаем
            raise AttributeError(name)
        return self._data.get(name, None)

    def __setattr__(self, name, value):
        if name.startswith('_'):
            # обычное присваивание для служебных атрибутов
            super().__setattr__(name, value)
        else:
            self._data[name] = value

Python определение методов (определение методов в python)

Теперь можно обращаться к несуществующим атрибутам как к ключам словаря:


sd = SmartDict()
sd.name = "Алиса"
print(sd.name)  # Алиса
print(sd.age)   # None (нет ключа)

Python print class (печать класса в python)

Проблемы:

  • В __setattr__ важно отличать служебные атрибуты (начинающиеся с подчёркивания) от обычных, иначе возникает бесконечная рекурсия при присваивании self._data.
  • Не вызывает исключение при попытке записи в несуществующий атрибут (по умолчанию).

Случаи использования: прокси, обёртки, объекты с динамическими полями (как в ORM).


Как упростить объявление свойств с помощью сторонних библиотек?

Библиотеки attrs и pydantic предоставляют декларативный способ описания атрибутов с валидацией, автоматически генерируя геттеры, сеттеры и методы инициализации.


# pip install attrs
import attr

@attr.s
class Point:
    x = attr.ib(validator=attr.validators.instance_of(int))
    y = attr.ib(validator=attr.validators.instance_of(int))

функции языка python (функции в python)

Класс Point автоматически получает корректные геттеры и сеттеры с проверкой типов.

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


p = Point(1, 2)
print(p.x, p.y)  # 1 2
# p.x = "string"  # TypeError: ...

Проблемы:

  • Внешняя зависимость - не подходит для простых проектов или библиотек.
  • Меньший контроль над поведением свойства (например, нельзя легко добавить вычисляемое поле).

Цели: быстрое прототипирование, уменьшение шаблонного кода, строгая валидация.

- Object attribute python (атрибуты объекта в python)
- Python call method (вызов метода в python)
- Python классы задания (задания на классы в python)

Ниже приведены расширенные примеры использования геттеров и сеттеров, демонстрирующие различные сценарии.

Вычисляемое свойство (площадь круга)

Пример

import math

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Радиус не может быть отрицательным")
        self._radius = value

    @property
    def area(self):
        return math.pi * self._radius ** 2

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

c = Circle(10)
print(c.area)  # 314.1592653589793
c.radius = 5
print(c.area)  # 78.53981633974483

Свойство только для чтения (без сеттера)

Пример

class ReadOnlyConfig:
    def __init__(self):
        self._config = {"host": "localhost", "port": 8080}

    @property
    def host(self):
        return self._config["host"]

    @property
    def port(self):
        return self._config["port"]

Сеттер не определён, поэтому попытка присвоить значение вызовет AttributeError.

cfg = ReadOnlyConfig()
print(cfg.host)  # localhost
# cfg.host = "newhost"  # AttributeError: can't set attribute

Ленивая загрузка (вычисление при первом обращении)

Пример

class LazyData:
    def __init__(self):
        self._heavy_data = None

    @property
    def data(self):
        if self._heavy_data is None:
            print("Вычисление данных...")
            self._heavy_data = [x ** 2 for x in range(1000)]
        return self._heavy_data
ld = LazyData()
print("До обращения")
print(len(ld.data))  # Вычисление данных... 1000
print(len(ld.data))  # 1000 (без повторного вычисления)

Валидация с несколькими условиями

Пример

class Temperature:
    def __init__(self, celsius):
        self._celsius = None
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError("Температура должна быть числом")
        if value < -273.15:
            raise ValueError("Температура ниже абсолютного нуля")
        self._celsius = value

    @property
    def fahrenheit(self):
        return self.celsius * 9 / 5 + 32
t = Temperature(100)
print(t.fahrenheit)  # 212.0
# t.celsius = -300  # ValueError: Температура ниже абсолютного нуля

Свойства в наследовании (переопределение геттера/сеттера)

Пример

class Base:
    def __init__(self):
        self._value = 0

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, val):
        self._value = val

class Derived(Base):
    @property
    def value(self):
        return super().value * 2

    @value.setter
    def value(self, val):
        super(Derived, type(self)).value.__set__(self, val + 1)
d = Derived()
d.value = 5
print(d.value)  # 12  ( (5+1)*2 )

Обратите внимание на вызов сеттера базового класса через super(Derived, type(self)).value.__set__.

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

Пример

class Secure:
    def __init__(self, key):
        self._key = key

    @property
    def key(self):
        return "***"

    @key.deleter
    def key(self):
        if input("Введите пароль для удаления ключа: ") == "admin":
            del self._key
            print("Ключ удалён")
        else:
            print("Доступ запрещён")
s = Secure("secret")
print(s.key)  # ***
del s.key     # запрос пароля …

Свойство с кэшированием результата (мемоизация)

Пример

class Fibonacci:
    def __init__(self):
        self._cache = {0: 0, 1: 1}

    @property
    def fib_10(self):
        if 10 not in self._cache:
            print("Вычисление fib(10)...")
            self._cache[10] = self._fib(10)
        return self._cache[10]

    def _fib(self, n):
        if n in self._cache:
            return self._cache[n]
        self._cache[n] = self._fib(n-1) + self._fib(n-2)
        return self._cache[n]
f = Fibonacci()
print(f.fib_10)  # Вычисление fib(10)... 55
print(f.fib_10)  # 55 (из кэша)

Геттер и сеттер с логированием

Пример

import logging
logging.basicConfig(level=logging.INFO)

class LoggerAttr:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        logging.info(f"Чтение value: {self._value}")
        return self._value

    @value.setter
    def value(self, new_val):
        logging.info(f"Запись value: {new_val}")
        self._value = new_val
la = LoggerAttr(10)
print(la.value)  # INFO:root:Чтение value: 10 -> 10
la.value = 20    # INFO:root:Запись value: 20

Такой подход используется для отладки или аудита изменений.

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

En
Get set python (python)