W3docs

Python @property: геттеры и сеттеры

Изучите декоратор @property в Python: создавайте геттеры, сеттеры, делитеры и вычисляемые атрибуты с валидацией данных.

Декоратор @property — это встроенный механизм Python для превращения метода в управляемый атрибут. Вместо того чтобы писать методы get_x() и set_x(), как в других языках, вы пишете обычный доступ к атрибуту (obj.x), сохраняя при этом полный контроль над тем, что происходит при чтении, записи или удалении этого атрибута.

В этой главе рассматривается:

  • Зачем нужны свойства и когда их использовать
  • Создание атрибута только для чтения с помощью @property
  • Добавление сеттера через @<name>.setter
  • Добавление делитера через @<name>.deleter
  • Вычисляемые (производные) свойства
  • Преобразование обычного атрибута в свойство без нарушения существующего кода
  • Встроенная функция property() — механизм, лежащий в основе декоратора
  • Как свойства работают как дескрипторы (краткий взгляд изнутри)
  • Распространённые ошибки

Прежде чем читать, убедитесь, что вы знакомы с классами и объектами Python. Свойства — ключевой инструмент для инкапсуляции в Python. Для методов уровня класса и статических методов см. @staticmethod и @classmethod.

Зачем нужны свойства

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

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

t = Temperature(25)
t.celsius = -5000   # nothing stops this — physically impossible

Проблема: ничто не мешает установить температуру ниже абсолютного нуля (−273.15 °C). Можно добавить метод set_celsius() с валидацией, но тогда вызывающий код должен измениться с t.celsius = 100 на t.set_celsius(100) — это нарушит обратную совместимость API.

@property решает эту проблему элегантно. Вы сохраняете синтаксис t.celsius = 100, добавляя слой контроля «за кулисами».

Базовый геттер: доступ только для чтения

Простейшее применение @propertyатрибут только для чтения, опирающийся на приватную переменную:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius   # store in a private attribute

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

Декоратор @property делает celsius похожим на обычный атрибут для вызывающего кода:

t = Temperature(25)
print(t.celsius)   # 25  — no parentheses; Python calls the getter automatically

Поскольку сеттера нет, попытка присвоить значение вызовет ошибку:

t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setter

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

Добавление сеттера с валидацией

Украсьте второй метод декоратором @<property_name>.setter, чтобы обрабатывать запись:

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

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

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError('Temperature below absolute zero')
        self._celsius = value

Теперь чтение и запись работают через обычный синтаксис атрибутов:

t = Temperature(25)
print(t.celsius)   # 25

t.celsius = 100
print(t.celsius)   # 100

t.celsius = -300   # ValueError: Temperature below absolute zero

Ключевое правило: сеттер и геттер должны иметь одинаковое имя (celsius в обоих случаях). Декоратор @celsius.setter связывает новый метод с существующим объектом свойства celsius.

Вычисляемые свойства

Свойство не обязано соответствовать какому-либо хранимому атрибуту. Оно может вычислять значение на лету на основе других данных:

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

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

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError('Temperature below absolute zero')
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

fahrenheit не имеет backing-переменной — его значение выводится из _celsius каждый раз при чтении:

t = Temperature(0)
print(t.fahrenheit)   # 32.0

t.celsius = 100
print(t.fahrenheit)   # 212.0

Поскольку @fahrenheit.setter не определён, попытка записать t.fahrenheit = 100 вызовет AttributeError. Вычисляемые свойства по умолчанию доступны только для чтения, если вы явно не добавите сеттер.

Пример вычисляемого свойства из реальной практики

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError('Width must be positive')
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError('Height must be positive')
        self._height = value

    @property
    def area(self):
        return self._width * self._height   # computed; no setter

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)   # computed; no setter


r = Rectangle(4, 5)
print(r.area)       # 20
print(r.perimeter)  # 18

r.width = 10
print(r.area)       # 50

r.width = -1        # ValueError: Width must be positive

Добавление делитера

Декоратор @<property_name>.deleter позволяет выполнить код, когда вызывающий код использует del obj.attr:

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

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

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError('Temperature below absolute zero')
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        print('Deleting celsius')
        del self._celsius


t = Temperature(25)
del t.celsius           # Deleting celsius
print(t.celsius)        # AttributeError: 'Temperature' object has no attribute '_celsius'

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

  • Удаление кэшированного значения для принудительного пересчёта при следующем обращении.
  • Явное освобождение ресурсов, связанных с атрибутом.
  • Обеспечение того, что однажды удалённое значение не может быть прочитано без повторного присваивания.

Преобразование обычного атрибута в свойство

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

# Version 1 — plain attribute, no validation
class Circle:
    def __init__(self, radius):
        self.radius = radius

c = Circle(5)
print(c.radius)   # 5
c.radius = 10     # works, but nothing stops c.radius = -1

Позже вам нужна валидация. С @property можно добавить её, не затрагивая вызывающий код:

# Version 2 — property with validation; public interface unchanged
import math

class Circle:
    def __init__(self, radius):
        self.radius = radius   # this now calls the setter

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

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError('Radius cannot be negative')
        self._radius = value

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


c = Circle(5)
print(c.radius)          # 5
print(f'{c.area:.4f}')   # 78.5398

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

c.radius = -1            # ValueError: Radius cannot be negative

Любой существующий код, который читает или записывает c.radius, продолжает работать без изменений.

Встроенная функция property()

@property — это синтаксический сахар для встроенной функции property(). Следующие два определения эквивалентны:

# --- decorator style (recommended) ---
class Person:
    def __init__(self, age):
        self._age = age

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

    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError('Age must be a non-negative integer')
        self._age = value
# --- property() style (explicit) ---
class Person:
    def __init__(self, age):
        self._age = age

    def _get_age(self):
        return self._age

    def _set_age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError('Age must be a non-negative integer')
        self._age = value

    def _del_age(self):
        del self._age

    age = property(_get_age, _set_age, _del_age, 'The person\'s age in years')

property(fget, fset, fdel, doc) принимает до четырёх аргументов: функцию-геттер, функцию-сеттер, функцию-делитер и строку документации. Любой из них может быть None.

p = Person(30)
print(p.age)               # 30
p.age = 31
print(p.age)               # 31
print(Person.age.__doc__)  # The person's age in years

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

Как работают свойства: краткий взгляд на дескрипторы

Внутренне property является дескриптором — объектом, который определяет __get__, __set__ и __delete__ на уровне класса. Когда Python ищет obj.attr, он проверяет, является ли атрибут класса дескриптором, и если да, вызывает его __get__ вместо того, чтобы вернуть значение напрямую.

Это можно увидеть, изучив объект свойства на уровне класса:

class Square:
    def __init__(self, side):
        self._side = side

    @property
    def side(self):
        return self._side

    @side.setter
    def side(self, value):
        if value < 0:
            raise ValueError('Side must be non-negative')
        self._side = value


print(type(Square.side))    # <class 'property'>
print(Square.side.fget)     # <function Square.side at 0x...>
print(Square.side.fset)     # <function Square.side at 0x...>
print(Square.side.fdel)     # None

Именно поэтому чтение Square.side возвращает сам объект свойства (дескриптор, обращение к которому происходит на уровне класса), тогда как чтение s.side на экземпляре запускает __get__ и возвращает целое число. Протокол дескрипторов — это тот же механизм, который используется classmethod, staticmethod и самими функциями. Для более глубокого изучения см. магические методы Python.

Распространённые ошибки

Бесконечная рекурсия: забытое подчёркивание

Очень распространённая ошибка — использование одного и того же имени как для свойства, так и для backing-атрибута:

class Bad:
    @property
    def value(self):
        return self.value   # RecursionError! This calls the getter again

    @value.setter
    def value(self, v):
        self.value = v      # RecursionError! This calls the setter again

Всегда храните backing-значение под другим именем, по соглашению с префиксом в виде подчёркивания:

class Good:
    @property
    def value(self):
        return self._value   # reads the private attribute

    @value.setter
    def value(self, v):
        self._value = v      # writes the private attribute

Сеттер определён раньше геттера

Декоратор сеттера @celsius.setter ссылается на объект свойства celsius, который должен существовать заранее. Всегда определяйте геттер (@property) перед сеттером и делитером в теле класса.

__init__ автоматически вызывает сеттер

Когда вы пишете self.radius = radius внутри __init__, Python вызывает сеттер (если он существует). Обычно это то, что вам нужно — валидация выполняется и при создании объекта. Но это означает, что ваш сеттер должен корректно обрабатывать начальное присваивание:

class Circle:
    def __init__(self, radius):
        self.radius = radius   # triggers the setter — validation applies here too

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

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError('Radius cannot be negative')
        self._radius = value

Circle(-1)   # ValueError: Radius cannot be negative

Свойства определяются на уровне класса, а не экземпляра

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

Краткий справочник

СинтаксисЧто делает
@propertyОпределяет геттер; атрибут становится доступным только для чтения до добавления сеттера
@<name>.setterОпределяет сеттер; атрибут становится доступным для чтения и записи
@<name>.deleterОпределяет делитер; del obj.attr запускает этот метод
property(fget, fset, fdel, doc)Эквивалентная встроенная функция без синтаксиса декоратора
ClassName.prop.fgetБазовая функция-геттер
ClassName.prop.fsetБазовая функция-сеттер (None, если сеттер не определён)
ClassName.prop.fdelБазовая функция-делитер (None, если делитер не определён)

Практика

Практика
Which decorator do you use to define a setter for a property named `age`?
Which decorator do you use to define a setter for a property named `age`?
Практика
What happens when you assign to a property that has only a getter defined?
What happens when you assign to a property that has only a getter defined?
Практика
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
Was this page helpful?