Термин инкапсуляция в контексте объектно-ориентированного программирования подразумевает концепцию объединения данных и соответствующих методов в единое целое внутри класса. В Python инкапсуляция реализуется как через классы, так и при работе с объектами. В некоторых других языках, таких как Java, инкапсуляция также предполагает скрытие свойств и методов, благодаря чему они становятся приватными. Это означает ограничение доступа к ним рамками одного класса или модуля.
В Python такой уровень инкапсуляции отсутствует, хотя возможна ее имитация. Прежде чем изучать методы достижения этой цели, стоит задуматься о причинах необходимости скрытия данных.
Классы иногда могут быть весьма объемными и обладать деталями, включающими множество вспомогательных полей и методов, которые не предназначены для использования за пределами самого класса. Они играют роль внутренних механизмов, необходимых для функционирования класса.
Кроме того, в ряде языков программирования считается хорошей практикой скрытие всех полей объектов, чтобы защитить их от прямой модификации из основной программы. Изменять и получать их значения следует исключительно через специализированные методы.
К примеру, если необходимо проверять корректность значения, присваиваемого полю, выполнять проверку в основном коде программы нецелесообразно. Логично поместить проверку в метод, который управляет присвоением значений полю. Тем самым поле будет защищено от некорректных значений.
Часто предпочитается скрывать поля самого класса, а не его объектов. Например, если класс отслеживает количество своих экземпляров, следует предотвратить случайные изменения этого счетчика извне. Представим пример с подобным счетчиком на языке Python.
class B: count = 0 def __init__(self): B.count += 1 def __del__(self): B.count -= 1 a = B() b = B() print(B.count) # выведет 2 del a print(B.count) # выведет 1
Код работает корректно, однако проблема возникает, если по ошибке поле B.count будет изменено в основной ветке:
… B.count -= 1 print(B.count) # будет выведен 0, хотя остался объект b
Для имитации скрытия атрибутов Python предлагает соглашение, согласно которому добавление двойного подчеркивания перед именем атрибута или метода сигнализирует, что он предназначен только для внутреннего использования:
class B: __count = 1 def __init__(self): B.__count += 1 def __del__(self): B.__count -= 1 a = B() print(B.__count)
Попытка выполнить этот код вызовет исключение:
File "test.py", line 9, in <module> print(B.__count) AttributeError: type object 'B' has no attribute '__count'
Атрибут __count недоступен за пределами класса, хотя внутри он открыто используется. Если мы не можем даже получить значение поля извне, изменить его тоже нельзя.
Фактически сокрытие в Python не абсолютное, и доступ к счетчику возможен посредством B._B__count
:
… print(B._B__count)
Существует согласование: если атрибут имеет два подчеркивания впереди имени, для доступа извне к имени добавляется имя класса с одним подчеркиванием. Это маскирует истинный атрибут. Вне класса он не существует. Программисты должны быть осторожны и не взаимодействовать с такими атрибутами, за исключением крайних случаев.
Теперь, защитив поле от случайных изменений, мы можем получить его значение через метод:
class B: __count = 0 def __init__(self): B.__count += 1 def __del__(self): B.__count -= 1 def qtyObject(): return B.__count a = B() b = B() print(B.qtyObject()) # будет выведено 2
В случае метода qtyObject() вызов осуществляется через класс, так как self у него отсутствует.
Точно так же методы можно сделать "приватными", применив двойное подчеркивание:
class DoubleList: def __init__(self, l): self.double = DoubleList.__makeDouble(l) def __makeDouble(old): new = [] for i in old: new.append(i) new.append(i) return new nums = DoubleList([1, 3, 4, 6, 12]) print(nums.double) print(DoubleList.__makeDouble([1,2]))
Результат будет следующим:
[1, 1, 3, 3, 4, 4, 6, 6, 12, 12] Traceback (most recent call last): File "test.py", line 13, in <module> print(DoubleList.__makeDouble([1,2])) AttributeError: type object 'DoubleList' has no attribute '__makeDouble'
Метод __setattr__()
В Python атрибуты можно присваивать объекту за пределами его класса:
>>> class A: ... def __init__(self, v): ... self.field1 = v ... >>> a = A(10) >>> a.field2 = 20 >>> a.field1, a.field2 (10, 20)
Если необходимо предотвратить такое поведение, можно использовать метод __setattr__()
для перегрузки операции
присваивания атрибуту:
>>> class A: ... def __init__(self, v): ... self.field1 = v ... def __setattr__(self, attr, value): ... if attr == 'field1': ... self.__dict__[attr] = value ... else: ... raise AttributeError ... >>> a = A(15) >>> a.field1 15 >>> a.field2 = 30 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 8, in __setattr__ AttributeError >>> a.field2 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'A' object has no attribute 'field2' >>> a.__dict__ {'field1': 15}
Поясним, что здесь происходит. Метод __setattr__(), если он включен в класс, всегда вызывается, когда атрибуту присваивается значение. Важно отметить, что назначение новому атрибуту также означает его добавление к объекту.
Когда создается объект a, в конструктор передается число 15, и атрибут field1 добавляется объекту. Присвоение влечет за собой вызов метода __setattr__(), где проверяется соответствует ли attr значению 'field1'. В случае соответствия атрибут добавляется в словарь атрибутов объекта.
Нельзя в __setattr__() напрямую указывать self.field1 = value
, так как это вызовет рекурсивный вызов метода
__setattr__(). Чтобы избежать этого, поле добавляется через словарь __dict__, который хранит все атрибуты объекта.
Если attr не соответствует допустимым полям, генерируется исключение AttributeError. Это объясняет, почему попытка назначения field2 заканчивается ошибкой.
Практическая работа
Создайте класс с "полной инкапсуляцией", где доступ к атрибутам и их изменения осуществляются через методы. В объектно-ориентированном программировании принято, чтобы имена методов для получения данных начинались с get, а для установки значений — с set. Например, getField, setField.