Инкапсуляция и property
Инкапсуляция по-питоновски
Инкапсуляция — скрыть внутреннее устройство объекта за понятным интерфейсом. В 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
Создайте класс 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 должен быть только для чтения"
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("Ниже абсолютного нуля")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
Что произойдёт?
class A:
def __init__(self):
self.__secret = 42
a = A()
print(a.__secret)
Что означает одно подчёркивание в начале имени (self._cache)?
Создайте класс 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
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("Ширина должна быть положительной")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Высота должна быть положительной")
self._height = value
@property
def area(self):
return self._width * self._height
Зачем в Python начинать с публичного атрибута, а не сразу писать геттеры и сеттеры?
Войдите, чтобы отмечать пройденные уроки.