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 + 32fahrenheit не имеет 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, если делитер не определён) |