Инкапсуляция
Инкапсуляция — это подход, при котором внутреннее устройство объекта скрывается, а взаимодействие с ним осуществляется через понятный и контролируемый интерфейс.
Инкапсуляция помогает:
- контролировать поведение объекта через ограниченные точки входа;
- защитить внутренние данные от прямого доступа и случайных изменений;
- упростить использование объектов без знания их внутренней структуры.
Уровни доступа к атрибутам
В Python существуют три уровня доступа, реализованные через соглашения об именовании:
Публичные атрибуты
Атрибуты без префикса. Доступны отовсюду: изнутри класса, снаружи и в наследниках.
class Book:
def __init__(self, title: str, author: str) -> None:
self.title = title # публичное поле
self.author = author # публичное поле
book = Book("1984", "George Orwell")
print(book.title) # доступ извне
book.title = "Animal Farm" # изменение извне
print(book.title)
Публичные атрибуты — часть открытого интерфейса объекта. Они не скрывают детали реализации.
Защищённые атрибуты
Обозначаются одним подчёркиванием перед именем: _name. Python не запрещает доступ к ним извне, но это сигнал программисту: «это внутренняя часть реализации, используйте осторожно».
class Book:
def __init__(self, title: str) -> None:
self._title = title # защищённый атрибут
def show_title(self) -> None:
print(self._title)
class SpecialBook(Book):
def show_title(self) -> None:
print(self._title.upper()) # доступ из наследника
book = SpecialBook("Brave New World")
print(book._title) # доступ извне технически возможен
book.show_title()
- Могут использоваться в дочерних классах.
- Не предназначены для прямого внешнего использования.
Приватные атрибуты
Обозначаются двумя подчёркиваниями перед именем: __name. Python автоматически меняет имя такого атрибута внутри класса, чтобы затруднить случайное использование извне и избежать конфликтов имён в наследниках.
class Book:
def __init__(self, title: str) -> None:
self.__title = title # приватный атрибут
def show_title(self) -> None:
print(self.__title)
class SpecialBook(Book):
def print_title(self) -> None:
print(self.__title.upper()) # ошибка: имя манглировано
book = SpecialBook("Brave New World")
book.show_title() # доступ через метод базового класса
# book.__title # AttributeError
# book.print_title() # AttributeError
Манглирование имён
Манглирование (name mangling) — механизм, при котором Python преобразует имя приватного атрибута __value в _ClassName__value внутри класса. Это работает только для имён, начинающихся с двух подчёркиваний и не заканчивающихся двумя подчёркиваниями.
class Book:
def __init__(self, title: str) -> None:
self.__title = title
book = Book("Brave New World")
# print(book.__title) # AttributeError
print(book._Book__title) # доступ через манглированное имя (не рекомендуется)
_ClassName__attr технически возможно, но нарушает принцип инкапсуляции. Делайте так только в исключительных случаях, например при отладке.
Уровни доступа к методам
Соглашения распространяются и на методы:
method()— публичный метод._method()— защищённый метод, внутренняя логика.__method()— приватный метод, строго скрытая логика.
class User:
def __init__(self, name: str) -> None:
self.name = name
self.__id = self.__generate_id()
def show_info(self) -> None:
print(f"{self.name} — ID: {self.__id}")
def __generate_id(self) -> str:
from random import randint
return f"user-{randint(1000, 9999)}"
user = User("Alice")
user.show_info()
# user.__generate_id() # AttributeError
# user._User__generate_id() # возможно, но нарушает инкапсуляцию
Геттеры и сеттеры
Геттеры и сеттеры — методы для чтения и изменения скрытых полей. Они позволяют добавить валидацию, логирование или преобразование данных.
class Temperature:
def __init__(self) -> None:
self.__celsius = 0
def get_celsius(self) -> float:
return self.__celsius
def set_celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Температура не может быть ниже абсолютного нуля")
self.__celsius = value
temp = Temperature()
temp.set_celsius(25)
print(temp.get_celsius())
# temp.set_celsius(-300) # ValueError
Преимущества:
- Гибкость: можно изменить внутреннюю логику, не меняя внешний интерфейс.
- Безопасность: проверка значений до сохранения.
- Поддержка инкапсуляции: прямой доступ к полю закрыт.
Декоратор @property
Декоратор @property позволяет управлять доступом к полю, сохраняя естественный синтаксис обращения к атрибуту.
class Temperature:
def __init__(self, value: float) -> None:
self.__celsius = 0
self.celsius = value # вызов сеттера внутри __init__
@property
def celsius(self) -> float:
return self.__celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Температура не может быть ниже абсолютного нуля")
self.__celsius = value
t = Temperature(25)
print(t.celsius) # вызов геттера
t.celsius = 15 # вызов сеттера
print(t.celsius)
# t.celsius = -500 # ValueError
@propertyсоздаёт геттер.@name.setterсоздаёт сеттер для того же свойства.- Имя метода становится именем «виртуального» атрибута.
Read-only свойства
Если объявить только геттер без сеттера, свойство становится доступным только для чтения.
class Temperature:
def __init__(self, celsius: float) -> None:
self.__celsius = celsius
@property
def celsius(self) -> float:
return self.__celsius
@property
def fahrenheit(self) -> float:
return self.__celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
# t.celsius = 100 # AttributeError
# t.fahrenheit = 100 # AttributeError
Ловушка: __init__ в обход сеттера
Сеттер защищает данные только тогда, когда через него действительно проходят. Если в __init__ присвоить значение напрямую приватному полю, проверка не выполнится:
class Person:
def __init__(self, age: int) -> None:
self.__age = age # ❌ присвоили напрямую — сеттер не вызван
@property
def age(self) -> int:
return self.__age
@age.setter
def age(self, value: int) -> None:
if value < 0:
raise ValueError("Возраст не может быть отрицательным")
self.__age = value
p = Person(-10)
print(p.age) # -10 — объект создан с некорректными данными!
Правильный вариант — присваивать «виртуальный» атрибут, тогда сеттер сработает уже при создании объекта:
class Person:
def __init__(self, age: int) -> None:
self.age = age # ✅ доступ строго через @age.setter
@property
def age(self) -> int:
return self.__age
@age.setter
def age(self, value: int) -> None:
if value < 0:
raise ValueError("Возраст не может быть отрицательным")
self.__age = value
p = Person(-10) # ValueError уже на этапе создания
Правило: если у поля есть сеттер с проверкой, обращайтесь к полю только через него — включая __init__.
Дескрипторы
@property — это частный случай дескриптора. Дескриптор — это класс, который управляет чтением, записью и удалением атрибута другого класса через методы __get__, __set__ и __delete__.
class Age:
def __get__(self, instance, owner):
if instance is None: # обращение через класс: Person.age
return self
return instance._age
def __set__(self, instance, value: int) -> None:
if value < 0:
raise ValueError("Возраст не может быть отрицательным")
instance._age = value
class Person:
age = Age() # дескриптор управляет атрибутом age
def __init__(self, age: int) -> None:
self.age = age # Age.__set__ вызывается всегда
p = Person(25)
print(p.age) # 25 — сработал Age.__get__
# p.age = -10 # ValueError — сработал Age.__set__
Что есть что в методах дескриптора:
self— экземпляр самого дескриптора (объектAge());instance— объект, у которого читают атрибут (p);owner— класс этого объекта (Person).
Фактически p.age = 25 превращается в вызов Age.__set__(дескриптор, p, 25) — а внутрь этого метода можно поместить любую логику проверки.
age = Age()) должно совпадать с именем, по которому обращаются (p.age) — иначе Python просто не найдёт дескриптор. Внутри дескриптора значение хранится под другим именем (_age): если написать instance.age = value, снова вызовется __set__ и получится бесконечная рекурсия.
Универсальный дескриптор: __set_name__
В примере выше имя _age зашито в код, поэтому дескриптор годится только для одного поля. Метод __set_name__ получает имя атрибута автоматически — и один класс подходит для любого количества полей:
class PositiveNumber:
def __set_name__(self, owner, name: str) -> None:
self.private_name = "_" + name # age → _age
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value: float) -> None:
if value < 0:
raise ValueError("Значение не может быть отрицательным")
setattr(instance, self.private_name, value)
def __delete__(self, instance) -> None:
delattr(instance, self.private_name)
class Person:
age = PositiveNumber()
balance = PositiveNumber()
def __init__(self, name: str, age: int, balance: float = 0) -> None:
self.name = name
self.age = age
self.balance = balance
person = Person("Viktor", 48, 1000)
print(person.age, person.balance) # 48 1000
print(person.__dict__) # {'name': 'Viktor', '_age': 48, '_balance': 1000}
person.balance = 1500 # проверка выполняется
# person.age = -10 # ValueError
Одна проверка «значение неотрицательно» описана один раз и работает сразу для двух полей — и будет работать в любом другом классе, где объявлен такой же дескриптор.
Почему значение нельзя хранить в самом дескрипторе
Это самая опасная ошибка при работе с дескрипторами. Экземпляр дескриптора создаётся один раз на класс, а не на каждый объект — поэтому сохранённое в нём значение станет общим для всех объектов:
class BrokenNumber:
def __set__(self, instance, value: float) -> None:
self.value = value # ❌ значение осело в самом дескрипторе
def __get__(self, instance, owner):
return self.value
class Box:
size = BrokenNumber()
first = Box()
second = Box()
first.size = 1
second.size = 2
print(first.size) # 2 — значение second перезаписало значение first!
Правильно всегда складывать данные в instance — у каждого объекта своё хранилище:
setattr(instance, self.private_name, value) # или instance._age = value
Проверка типа и запрет удаления
Дескриптор — удобное место и для проверки типа. Разные нарушения полезно разделять: неподходящий тип — это TypeError, недопустимое значение — ValueError:
class PositiveNumber:
def __set_name__(self, owner, name: str) -> None:
self.name = name
self.private_name = "_" + name
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value: float) -> None:
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise TypeError(f"{self.name} должно быть числом")
if value <= 0:
raise ValueError(f"{self.name} должно быть больше нуля")
setattr(instance, self.private_name, value)
def __delete__(self, instance) -> None:
raise AttributeError("Удаление размера запрещено")
class Rectangle:
width = PositiveNumber()
height = PositiveNumber()
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
rectangle = Rectangle(4, 5)
# Rectangle(0, 5) # ValueError: width должно быть больше нуля
# rectangle.height = "пять" # TypeError: height должно быть числом
# del rectangle.width # AttributeError: Удаление размера запрещено
__delete__ нужен только тогда, когда для del требуется особое поведение — удалить значение иначе или, как здесь, запретить удаление совсем.
Когда @property, а когда дескриптор
| Ситуация | Что выбрать |
|---|---|
| Логика нужна одному полю одного класса | @property — короче и понятнее |
| Одинаковая проверка у нескольких полей или классов | дескриптор — логика пишется один раз |
| Нужны валидаторы «на все случаи» (положительное число, длина строки, диапазон) | дескриптор |
| Логирование, кэширование, ленивые вычисления при доступе к полю | дескриптор |
| Требуется отделить управление атрибутом от бизнес-логики класса | дескриптор |