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

Под инкапсуляцией в объектно-ориентированном программировании понимается упаковка данных и методов для их обработки вместе, т. е. в классе. В 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 = 0
    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)

Таково соглашение. Если в классе есть атрибут с двумя первыми подчеркиваниями, то для доступа извне к имени атрибута добавляется имя класса с одним впереди стоящим подчеркиванием. В результате атрибут как он есть (в данном случае __count) оказывается замаскированным. Вне класса такого атрибута просто не существует. Для программиста же наличие двух подчеркиваний перед атрибутом должно сигнализировать, что трогать его вне класса не стоит вообще, даже через _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__(), где проверяется соответствует ли имя атрибута строке 'field1'. Если так, то атрибут и соответствующее ему значение добавляется в словарь атрибутов объекта.

Нельзя в __setattr__() написать просто self.field1 = value, так как это приведет к новому рекурсивному вызову метода __setattr__(). Поэтому поле назначается через словарь __dict__, который есть у всех объектов, и в котором хранятся их атрибуты со значениями.

Если параметр attr не соответствует допустимым полям, то искусственно возбуждается исключение AttributeError. Мы это видим, когда в основной ветке пытаемся обзавестись полем field2.

Практическая работа

Разработайте класс с "полной инкапсуляцией", доступ к атрибутам которого и изменение данных реализуются через вызовы методов. В объектно-ориентированном программировании принято имена методов для извлечения данных начинать со слова get (взять), а имена методов, в которых свойствам присваиваются значения, – со слова set (установить). Например, getField, setField.

Курс с примерами решений практических работ и всеми уроками: android-приложение, pdf-версия.

Комментарии

class a:
    __k=15
    def __init__(self,r):
        self.__r=r
    def set_r(self,n):
        self.__r=n
    def set_k(self,n):
        self.__k=n
    def get_r(self):
        return self.__r
    def get_k(self):
        return self.__k
print(a._a__k)
a._a__k=4
print(a._a__k)
o=a(8)
print(o._a__k, o._a__r)
print(o.get_k(),o.get_r())
o.set_r(9)
o.set_k(7)
print(o._a__k, o._a__r)
print(o.get_k(),o.get_r())

Ответ на от Александр

Думаю, да. Только надо иметь в виду, что у вас в программе два разных атрибута __k. Один принадлежит классу, другой объекту.

print(a._a__k, o._a__k)
4 7

Ответ на от plustilino

Наверное было правильно использовать статические методы для изменения свойства класса и получения его значения
class a:
    __k=15
    def __init__(self,r):
        self.__r=r
    def set_r(self,n):
        self.__r=n
    def get_r(self):
        return self.__r
    @staticmethod
    def set_k(n):
        a.__k = n
    @staticmethod
    def get_k():
        return a.__k
print(a._a__k) # 15
a._a__k=4
print(a._a__k) # 4
o=a(8) #
print(o._a__k, o._a__r) # 4 8
print(o.get_k(),o.get_r()) # 4 8
o.set_r(9)
o.set_k(7)
print(o._a__k, o._a__r) # 7 9
print(o.get_k(),o.get_r()) # 7 9
print(a._a__k, o._a__k) # 7 7

Мне кажется или при таком инкапсулировании вообще до атрибутов снаружи не добраться для установки, а читать можно?
class Ball:
    def __init__(self, x, y):
        self.__dict__["x"] = x
        self.__dict__["y"] = y
    def __setattr__(self, key, value):
        if self.__dict__.get(key) == None:
            print(f"Object have not attribute {key}")
        else:
            print(f"You mast use method setCoord(x,y) for attribute {key}")
    def setCoord(self, x, y):
        self.__dict__["x"] = x
        self.__dict__["y"] = y
    def __str__(self):
        return (f"Ball at ({self.x}, {self.y})")
 
a = Ball(5, 7)
print(a)
 
a.x = 100
 
 
a.proba = 100
 
a.setCoord(10, 15)
print(a)

Подскажите, пожалуйста, правильно ли я отразила смысл инкапсуляции в практическом задании. Или наоборот смысл в том, чтобы "пользователь" не имел доступа к вводу данных?
n = 0
 
class Entry():
    def __init__(self):
        Entry.__n = n+1
    def getEntry(self):
        return Entry.__n, Entry.__login, Entry.__password 
    def setEntry(self):
        Entry.__login = input("Ведите имя: ")
        Entry.__password = input("Введите пароль: ")
 
while input("Для добавления имени и паролz, нажмите 1: ") == "1":
    a = Entry()
    n += 1
    a.setEntry()
    print("Введённые данные", a.getEntry())
 
input("Чтобы выйти, нажмите Enter")

class FullEncapsulation:
    def __init__(self):
        self.__name = "Джон"
        self.__surname = "Локк"
 
    def setField(self, n, s):
        self.__name = n
        self.__surname = s
 
    def getfield(self):
        return FullEncapsulation.__name, FullEncapsulation.__surname
 
b = FullEncapsulation()
b.setField("John", "Weak")
print(b.getfield())

class Soul:
    __secret = 'none'    
    def __setattr__(self, key, value):
        if key == '_Soul__secret':
            self.__dict__[key] = value
        else:
            raise AttributeError
    def setSecret(self, s):
        self.__secret = str(s)
    def getSecret(self):
        return self.__secret
a = Soul()
print(a.getSecret())
a.setSecret('generosity')
print(a.getSecret())

 

class Calculate:
    __usage = 0

    def __init__(self, a, b):
        self.__val = Calculate.__calk(a, b)

    def _setattr__(self, attr, value):
        if attr == 'val':
            self.__dict__[attr] = value
        else:
            raise AttributeError

    def __calk(a, b):
        return a * b / 2 * b

    def get_val(self):
        return self.__val

    def set_usage(n):
        Calculate.__usage = n

    def get_usage():
        return Calculate.__usage


i = 0

while i != '':
    i += 1
    q = input("First number\n")
    if q == 'exit':
        print(f"Goodbye. You used the function {Calculate.get_usage()} times")
        break
    w = input("Second number\n")
    if w == 'exit':
        print(f"Goodbye. You used the function {Calculate.get_usage()} times")
        break
    q = int(q)
    w = int(w)
    print(Calculate(q, w).get_val())
    Calculate.set_usage(i)

 

class Calculate:
    __usage = 0
 
    def __init__(self, a, b):
        self.__val = Calculate.__calk(a, b)
 
    def _setattr__(self, attr, value):
        if attr == 'val':
            self.__dict__[attr] = value
        else:
            raise AttributeError
 
    def __calk(a, b):
        return a * b / 2 * b
 
    def get_val(self):
        return self.__val
 
    def set_usage(n):
        Calculate.__usage = n
 
    def get_usage():
        return Calculate.__usage
 
 
i = 0
 
while i != '':
    i += 1
    q = input("First number\n")
    if q == 'exit':
        print(f"Goodbye. You used the function {Calculate.get_usage()} times")
        break
    w = input("Second number\n")
    if w == 'exit':
        print(f"Goodbye. You used the function {Calculate.get_usage()} times")
        break
    q = int(q)
    w = int(w)
    print(Calculate(q, w).get_val())
    Calculate.set_usage(i)

class Point:
    def __init__(self, x, y):
        if not self.__is_number(x) or not self.__is_number(y):
            raise TypeError
 
        self.x = x
        self.y = y
 
    def __str__(self):
        return f'({self.x}, {self.y})'
 
    def get_x(self):
        return self.x
 
    def get_y(self):
        return self.y
 
    def set_x(self, x):
        self.x = x
 
    def set_y(self, y):
        self.y = y
 
    def __setattr__(self, attr, value):
        if attr in ('x', 'y', 'X', 'Y'):
            self.__dict__[attr.lower()] = value
        else:
            raise AttributeError
 
    def distance(self, point):
        return self.__sqrt_of_sqr_sums(
            self.x - point.get_x(), self.y - point.get_y())
 
    def __sqrt_of_sqr_sums(self, a, b):
        return (a * a + b * b) ** 0.5
 
    def __is_number(self, value):
        return type(value) in (int, float)

print(end='\n'*5)
 
import itertools
import time
import sys
 
 
#Собственно сама суть:
 
#----------------------------------------------------------------------------------------
class A:
 
	def set_value (self,a,b,c):
		self.__obj1=a # для вызова a._A__obj1
		self.__obj2=b
		self.__obj3=c
 
 
	def get_value (self,l):		# self._A__obj3
		if  l == 'obj1':
			return(self._A__obj1)
		if l== 'obj2':
			 return(self._A__obj2)
		if l== 'obj3':
			return(self._A__obj3)
 
 
a=A() # Cоздадим объект.
 
a.set_value(3,4,5) # Присвоим полям значения.
 
 
print('Видим значение первого поля:',a.get_value('obj1'),end='\n\n')
time.sleep(0.01)
print('Видим значение второго поля:',a.get_value('obj2'),end='\n\n')
time.sleep(0.01)
print('Видим значение третьего поля:',a.get_value('obj3'),end='\n\n')
time.sleep(0.01)
print('[И все эти поля разумеется скрыты, но их можно получить \
	\nиспользуя специальную функцию (в моём случае это get_value()).]', end='\n\n\n\n')
 
#--------------------------------------------------------------------------------------------
 
 
 
 
def Loading(): # Эксперементальная симуляция загрузки...забавы ради :)
	it = itertools.cycle(['...|'] +['\b\b\b\b']+['.../']+['\b\b\b\b']+['...-']+['\b\b\b\b']+['...\\']+['\b\b\b\b'])
	print('Loading ', end='')
	for x in range(65):      # 65 потому что ((n^2*4) + 1)....где n = кол.-во символов каждого "кадра"([...|] -4 символа.)
							 # и +1 потому что происходит итерация по индексам сформированных из range(65), т.е от 0 до 64.
		time.sleep(0.03)  # выполнение функции
		if x != 64:
		    print(next(it), end='', flush=True)
		elif x == 64:
		    print(next(it), end='\b\b\b\b :) ', flush=True)
 
def conclusion(M,n=0.01): # Фу-ция позволяет последовательно выводить
						  # символы указанной строки, внутри списка
	M=''.join(M)		  # с заданной скоростью (по умолчанию 1 санти-сек.).
	M=list(M)
 
	for i in M:
		print(''.join(i),end='')
		sys.stdout.flush()
		time.sleep(n)
	return ''
 
conclusion('АКТИВИРОВАНА ПРОГРАММА УНИЧТОЖЕНИЯ ЧеЛОВЕЧЕСКОЙ ЦИВИЛИЗАЦИИ !!!')
print(end='\n\n')
Loading()
print(end='\n\n')
A1=['                 =##=:                                 :+##@', 
'              +#######                                 %######=', 
'              +#######                                 %######%', 
'               .====###%*        :++@####++*        :=###====- ',
'                     -####-   +###############%    ####*', 
'                       %###= ###################.+####', 
'                        .*.:%###################%* *- ', 
'                           *#####################=', 
'                          ###@+   .@#####@-   :@###', 
'                          ###-      =###@       ###', 
'                          ###-    .#######:     ###', 
'                          +@##@++@##########++%##@=', 
'                           *##########.%#########= ', 
'                           *########+...:########=', 
'                             *%###############@+.', 
'                                #############-', 
'                             #%:.=#@##@##@#@ -=#.', 
'                        :@-  ###  V  V V  V  ###.  @*', 
'                       @###%   ## /\\ /\\ /\\  ###%  +####', 
'                    :=###%-   :%#############%*    %###%*', 
'               .#######*.       .=#########@..      .-#######-', 
'              +#######            .#######:            %######%', 
'              :++@####                                 %####++*', 
'                 =##*                                   -##@']
print(end='\n\n')
 
 
for i in A1:
	print(i)
	time.sleep(0.03)
 
Loading()
 
print(end='\n\n')
conclusion('ШУТКА :)')
 
 
 
 
 
 
 
 
 
 
 
print(end='\n'*5)

from math import pi
 
 
class Cylinder:
    @staticmethod
    def make_area(d, h):
        circle = pi * d ** 2 / 4
        side = pi * d * h
        return round(circle * 2 + side, 2)
 
    def __init__(self, dia, h):
        self.dia = dia
        self.h = h
        self.area = self.make_area(dia, h)
 
    def __setattr__(self, attr, value):
        if attr == 'dia':
            self.__dict__[attr] = value
            self.area = self.make_area(value, self.h)
        elif attr == 'h':
            self.__dict__[attr] = value
            self.area = self.make_area(self.dia, value)
        else:
            raise AttributeError

Очень неодназначная штука с инкапсуляцией. На моих интерпретаторах(с unix и win) работает по-другому: поля с obj.__attr видно прекрасно вне класса при присвоении. Ошибка AttributeError будет лишь при попытке получить значения поля obj.__attr, при этом ошибка пропадет(то есть поле станет видимым вне класса) если хоть раз сделать ему присвоение вне класса. Без всяких _obj__attr.
#обьявляем класс
class Full:
	def __init__(self, field):
		self.__field = field
	#сеттер поля field
	def setField(self, field):
		self.__field = field
	#геттер поля field
	def getField(self):
		return(self.__field)
#скрипт
#создаем обьект нашего класса
full = Full(8)
#setField
full.setField(3)
print("установим __field с помощью setField и получим значение с помощью getField {}".format(full.getField()))
#попробуем получить значения поля напрямую
try:
	print(full.__field)
except AttributeError:
	print("обращение к obj.__field напрямую чтоб получить значение - ошибка аттрибута")
	full.__field = 5
	print("теперь присвоим полю obj.__field значение из кода вне класса и вуаля - теперь поле видно вне класса несмотря на двойное подчеркивание")
	print(full.__field)
Так же есть неясности с __setattr__, все работает до тех пор пока в __init__ не появятся поля с __attr. Например:
class A:
	def __init__(self, x):
		self.__x = x
	def __setattr__(self, attr, value):
		assert attr=="__x", ("а тут у нас проблемки")
		if attr=="__x":
			self.__dict__[attr]=value
		else:
			raise AttributeError
a=A(5)
Поставил assert и стало ясно что в attr=="__x" возвращает false, следовательно в __init__ , при присвоении полю "__x" значения "x" в __setattr__ attr передается не "__x". Буду ковырять это дальше параллельно с остальными уроками. Возможно переопределение __setattr__ надо сделать по другому алгоритму или мое понимание вопроса еще слишком низко. Хотя, как я понимаю, сокрытие полей и методов вещь совершенно не обязательная и делается лишь по соглашению и для удобства понимания/отладки. Буду рад замечаниям.