Инкапсуляция и property

⏱ 30 мин · 🧩 заданий: 5

Инкапсуляция по-питоновски

Инкапсуляция — скрыть внутреннее устройство объекта за понятным интерфейсом. В Java или C++ для этого есть private. В Python — соглашения: «мы все здесь взрослые люди».

Запись Смысл Ограничение
name публичный атрибут нет
_name внутренний, «не трогайте снаружи» только соглашение
__name искажение имени (name mangling) имя превращается в _Класс__name
__name__ специальные (dunder) имена Python не придумывайте свои такие
class Account:
    def __init__(self, owner):
        self.owner = owner          # публичный
        self._history = []          # внутренний
        self.__pin = "1234"         # искажённое имя

a = Account("Анна")
print(a.owner, a._history)          # _history доступен — просто «не принято»
print(a.__dict__)                   # видно, во что превратилось __pin
print(a._Account__pin)              # и доступ всё равно возможен

Зачем нужно искажение имён

__name нужен не для секретности, а чтобы избежать конфликтов имён при наследовании:

class Base:
    def __init__(self):
        self.__id = "base"          # станет _Base__id

    def base_id(self):
        return self.__id

class Child(Base):
    def __init__(self):
        super().__init__()
        self.__id = "child"         # станет _Child__id — не затрёт атрибут родителя

c = Child()
print(c.base_id(), c.__dict__)

property

@property превращает метод в атрибут: чтение obj.x вызывает функцию. Можно добавить сеттер с проверкой:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price          # уже проходит через сеттер!

    @property
    def price(self):                # геттер: obj.price
        return self._price

    @price.setter
    def price(self, value):         # сеттер: obj.price = value
        if value < 0:
            raise ValueError("Цена не может быть отрицательной")
        self._price = round(value, 2)

    @property
    def price_with_vat(self):       # вычисляемое, только для чтения
        return round(self._price * 1.2, 2)

p = Product("Кофе", 199.999)
print(p.price, p.price_with_vat)
p.price = 250
print(p.price, p.price_with_vat)
try:
    p.price = -1
except ValueError as e:
    print("ValueError:", e)
try:
    p.price_with_vat = 0
except AttributeError as e:
    print("AttributeError:", e)

Снаружи p.price выглядит как обычный атрибут — но под ним логика.

Сначала атрибут, потом свойство

В Python не пишут «на всякий случай» get_price()/set_price(). Делают публичный атрибут, а если позже понадобилась проверка — превращают его в property. Код, использующий obj.price, менять не придётся.

Удаление и полный синтаксис

class User:
    def __init__(self, email):
        self._email = email

    def _get_email(self):
        return self._email

    def _set_email(self, value):
        if "@" not in value:
            raise ValueError("Некорректный email")
        self._email = value.lower()

    def _del_email(self):
        print("email удалён")
        self._email = None

    email = property(_get_email, _set_email, _del_email, "Адрес почты")   # то же без декораторов

u = User("a@x.ru")
u.email = "ANNA@X.RU"
print(u.email)
del u.email
print(User.email.__doc__)

property — это дескриптор (урок «Дескрипторы и slots» объяснит, как он работает).

cached_property

Для дорогих вычислений, результат которых не меняется, — functools.cached_property: вычисляется при первом обращении и сохраняется в __dict__ экземпляра.

from functools import cached_property

class Report:
    def __init__(self, rows):
        self.rows = rows

    @cached_property
    def total(self):
        print("  считаю total...")
        return sum(self.rows)

r = Report([1, 2, 3])
print(r.total)
print(r.total)          # второй раз — из кеша, без вычисления
del r.total             # сбросить кеш
print(r.total)

Только для чтения

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

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

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

c = Circle(2)
print(c.radius, round(c.area, 2))
try:
    c.radius = 5
except AttributeError as e:
    print(e)

Для полностью неизменяемых объектов удобнее @dataclass(frozen=True) или NamedTuple.

Итог

  • В Python нет настоящего private: _name — соглашение, __name — искажение имени против конфликтов при наследовании.
  • @property превращает метод в атрибут; @x.setter добавляет проверку при присваивании.
  • Свойство без сеттера — только для чтения; вычисляемые атрибуты — тоже через property.
  • Начинайте с публичных атрибутов, превращайте в property по необходимости.
  • cached_property — ленивое вычисление с кешем в экземпляре.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Создайте класс Temperature с атрибутом-свойством celsius: - при присваивании значения ниже −273.15 бросается ValueError; - свойство fahrenheit только для чтения вычисляется как celsius * 9 / 5 + 32. Значение храните во внутреннем атрибуте _celsius.

Какие тесты проверяют код (4)
t = Temperature(100); assert t.fahrenheit == 212
t = Temperature(0); t.celsius = 37; assert t.celsius == 37
try:
    Temperature(-300)
    ok = False
except ValueError:
    ok = True
assert ok, "Temperature(-300) должен бросать ValueError"

t = Temperature(10)
try:
    t.fahrenheit = 50
    ok = False
except AttributeError:
    ok = True
assert ok, "fahrenheit должен быть только для чтения"

Задание 2 Один вариант ответа

Что произойдёт?

class A:
    def __init__(self):
        self.__secret = 42

a = A()
print(a.__secret)
Задание 3 Один вариант ответа

Что означает одно подчёркивание в начале имени (self._cache)?

Задание 4 Программа на Python

Создайте класс Rectangle(width, height) со свойством area (вычисляемое, только чтение) и свойствами width/height, которые при присваивании отрицательного числа или нуля бросают ValueError.

Какие тесты проверяют код (2)
r = Rectangle(3, 4); assert r.area == 12; r.width = 5; assert r.area == 20
r = Rectangle(1, 1)
try:
    r.height = 0
    ok = False
except ValueError:
    ok = True
assert ok

Задание 5 Один вариант ответа

Зачем в Python начинать с публичного атрибута, а не сразу писать геттеры и сеттеры?

Войдите, чтобы отмечать пройденные уроки.