← Назад к вопросам

Зачем нужна функция property() в Python?

2.0 Middle🔥 301 комментариев
#Python Core#Асинхронность и многопоточность#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Функция property() в Python

property() — это встроенная функция, которая позволяет создавать управляемые атрибуты класса. Она превращает методы в свойства, позволяя вам вызывать методы как обычные атрибуты, но с дополнительной логикой валидации, преобразования или ограничения доступа.

Проблема без property

# ❌ Плохо: прямой доступ к атрибутам

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

person = Person('Alice', 30)
person.age = -5  # Ничто не помешает!
print(person.age)  # -5 — невалидное значение

# Проблемы:
# - Нет валидации
# - Нет логики при установке значения
# - Если позже добавим логику, сломаем существующий код

Решение 1: Методы (старый способ)

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age  # Приватный атрибут
    
    def get_age(self):
        return self._age
    
    def set_age(self, value):
        if value < 0 or value > 150:
            raise ValueError('Age must be 0-150')
        self._age = value

person = Person('Alice', 30)
person.set_age(35)  # Валидируется
print(person.get_age())  # 35

# Проблемы:
# - Неудобный API (get_/set_)
# - Много кода

Решение 2: property() — правильный способ

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age  # Приватный атрибут с _
    
    @property
    def age(self):
        """Getter: вызывается при person.age"""
        return self._age
    
    @age.setter
    def age(self, value):
        """Setter: вызывается при person.age = value"""
        if value < 0 or value > 150:
            raise ValueError('Age must be 0-150')
        self._age = value
    
    @age.deleter
    def age(self):
        """Deleter: вызывается при del person.age"""
        print('Age deleted')
        del self._age

# Использование как обычный атрибут
person = Person('Alice', 30)
print(person.age)      # 30 — вызывает getter
person.age = 35        # Вызывает setter с валидацией
print(person.age)      # 35

person.age = -5        # ValueError: Age must be 0-150
del person.age         # Вызывает deleter

Декоратор @property vs property() функция

# Способ 1: Декоратор @property (современный)
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('Radius must be positive')
        self._radius = value

# Способ 2: Функция 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('Radius must be positive')
        self._radius = value
    
    radius = property(get_radius, set_radius)

# Оба способа работают одинаково

Практические примеры

1. Валидация при установке

class BankAccount:
    def __init__(self, balance=0):
        self._balance = balance
    
    @property
    def balance(self):
        return self._balance
    
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError('Balance cannot be negative')
        self._balance = value
    
    def withdraw(self, amount):
        self.balance -= amount  # Использует setter

account = BankAccount(1000)
print(account.balance)  # 1000
account.balance = 500   # OK
account.balance = -100  # ValueError!

2. Вычисляемые свойства (readonly)

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """Площадь вычисляется, не хранится"""
        return self.width * self.height
    
    @property
    def perimeter(self):
        """Периметр вычисляется на лету"""
        return 2 * (self.width + self.height)

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

rect.area = 30         # AttributeError: can't set attribute

3. Ленивая загрузка (lazy loading)

class User:
    def __init__(self, user_id):
        self.user_id = user_id
        self._profile = None
    
    @property
    def profile(self):
        """Загружаем профиль только когда нужен"""
        if self._profile is None:
            print(f'Loading profile for user {self.user_id}')
            # Загружаем из БД или API
            self._profile = fetch_profile(self.user_id)
        return self._profile

user = User(123)
print(user.user_id)  # 123
print(user.profile)  # Loading profile for user 123
print(user.profile)  # Уже загружено, не грузит снова

4. Кэширование значений

class DataProcessor:
    def __init__(self, data):
        self._data = data
        self._cached_result = None
    
    @property
    def result(self):
        """Результат вычисляется один раз"""
        if self._cached_result is None:
            print('Computing result...')
            self._cached_result = sum(self._data) / len(self._data)
        return self._cached_result

processor = DataProcessor([1, 2, 3, 4, 5])
print(processor.result)  # Computing result... 3.0
print(processor.result)  # 3.0 (из кэша)

5. Защита от изменений

class ImmutablePoint:
    def __init__(self, x, y):
        self._x = x
        self._y = y
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    # Нет setter — координаты неизменяемы

point = ImmutablePoint(3, 4)
print(point.x)  # 3
point.x = 5     # AttributeError: can't set attribute

6. Трансформация данных

from datetime import datetime

class Post:
    def __init__(self, title, created_at):
        self.title = title
        self._created_at = created_at  # datetime объект
    
    @property
    def created_at_iso(self):
        """Форматируем дату при получении"""
        return self._created_at.isoformat()
    
    @property
    def created_at_friendly(self):
        """Человеко-читаемый формат"""
        return self._created_at.strftime('%d.%m.%Y %H:%M')

post = Post('Hello', datetime(2025, 1, 15, 10, 30))
print(post.created_at_iso)       # 2025-01-15T10:30:00
print(post.created_at_friendly)  # 15.01.2025 10:30

Сравнение: с и без property

# ❌ Без property — неудобно и небезопасно

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

temp = Temperature(25)
temp.celsius = -300  # Невозможная температура!

# ✅ С property — безопасно и удобно

class TemperatureSafe:
    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

temp = TemperatureSafe(25)
temp.celsius = -300  # ValueError!

Как property работает под капотом

class Descriptor:
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget  # Getter функция
        self.fset = fset  # Setter функция
        self.fdel = fdel  # Deleter функция
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
    
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

# property() — это встроенный Descriptor
class Person:
    def __init__(self, name):
        self._name = name
    
    def get_name(self):
        return self._name
    
    def set_name(self, value):
        if not value:
            raise ValueError('Name cannot be empty')
        self._name = value
    
    name = property(get_name, set_name)

# Эквивалентно:
# name = Descriptor(get_name, set_name)

Best Practices

1. Используй _ для приватных атрибутов

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # Приватный
    
    @property
    def balance(self):
        return self._balance

2. Документируй property

class Temperature:
    @property
    def celsius(self):
        """Температура в градусах Цельсия (readonly)."""
        return self._celsius

3. Дорогие операции кэшируй

class DataAnalysis:
    def __init__(self, data):
        self._data = data
        self._stats = None
    
    @property
    def statistics(self):
        """Статистика кэшируется (дорогая операция)."""
        if self._stats is None:
            self._stats = compute_statistics(self._data)
        return self._stats

4. Не делай side effects в getters

# ❌ Плохо: getter вызывает побочные эффекты
class Cache:
    @property
    def value(self):
        logger.info('Getting value')  # Side effect
        return self._value

# ✅ Хорошо: getter только возвращает значение
class Cache:
    @property
    def value(self):
        return self._value

Итоговое правило

property() используй когда:

  • Нужна валидация при установке значения
  • Нужно вычислить значение на лету
  • Нужна ленивая загрузка
  • Нужно изменить реализацию без изменения API
  • Нужно заблокировать запись (readonly свойства)

property() не используй когда:

  • Это простой атрибут без логики
  • property делает дорогую операцию (используй метод вместо этого)
  • Нужны side effects в getter