Определение
Значение термина «инкапсуляция» расплывчато и отличается от источника к источнику. Принято считать, что инкапсуляция — один из основополагающих принципов ООП, хотя некоторые научные статьи вовсе упускают инкапсуляцию из списка. К примеру, Джон Митчелл в книге «Концепты в языках программирования» при перечислении основных концептов в ООП упоминает только абстракцию — термин который принято считать близким к инкапсуляции по значению, но все-же более обширным и высокоуровневым. С другой стороны, Роберт Мартин в его книге «Чистая архитектура» явно говорит о том, что инкапсуляция, наследование и полиморфизм считается фундаментом ООП.
Разнообразие определений, данных термину «инкапсуляция», сложно привести к общему знаменателю. В целом можно выделить два подхода к значению этого термина. Инкапсуляция может быть рассмотрена как:
- связь данных с методами которые этими данными управляют;
- набор инструментов для управления доступом к данным или методам которые управляют этими данными.
Инкапсуляция как связь
Подобного рода трактовка термина «инкапсуляция» очень проста в объяснении. В данном случае, любой класс в котором есть хотя бы одна переменная и один метод который ею управляет наглядно демонстрирует этот принцип.
#!/usr/bin/python3 class Phone: number = "111-11-11" def print_number(self): print( "Phone number is: ", self.number ) my_phone = Phone() my_phone.print_number() input( "Press Enter to exit" )
Класс “Phone” объединяет данные в переменной “number” с методом “print_number()”
Можно создать класс, который состоит только из методов (и не содержит переменных), что может быть удобно в некоторых языках программирования. Также возможно создать класс содержащий только данные, без методов, чего, во многих случаях, следует избегать. Обе практики следует применять в случае необходимости и их отношение к «объединяющей» инкапсуляции спорно.
Инкапсуляция как управление доступом
Объяснение концепции ограничения доступа к данным или методам требует гораздо большего количества деталей. Прежде всего, в этом контексте термин «доступ» следует понимать как способность видеть и / или изменять внутреннее содержимое класса. Существует несколько уровней доступа, предоставляемых большинством ООП языков. Обобщая можно сказать что данные объекта могут быть:
- публичными ( public ) — данные доступны всем;
- приватными ( private ) — данные доступны только объекту/классу которому они принадлежат.
Большинство языков имеют дополнительные степени доступа, которые находятся между этими границами. К примеру, в C++ и Python3 есть три уровня доступа: публичный, защищенный и приватный; C# добавляет ключевое слово «внутренний» ( internal ) в список.
Стоит отметить, что в большинстве языков программирования, уровень доступа к любым данным установлен по умолчанию. Например, в C++ по умолчанию уровень доступа к данным в классе задан как приватный— к его данным могут обращаться только члены и друзья класса. Стандартный уровень доступа к структуре ( struct ) в C++ отличается — он публичный, и данные в такой структуре могут быть доступны любому. Уровень доступа для переменных и методов класса в Python 3 полностью зависит от синтаксиса.
Примеры
Инкапсуляция
Python 3 предоставляет 3 уровня доступа к данным:
- публичный ( public , нет особого синтаксиса, publicBanana );
- защищенный ( protected , одно нижнее подчеркивание в начале названия, _protectedBanana );
- приватный ( private , два нижних подчеркивания в начала названия, __privateBanana ).
Для краткости и простоты, только два базовых уровня (приватный и публичный) освещены в примере.
#!/usr/bin/python3 class Phone: username = "Kate" # public variable __how_many_times_turned_on = 0 # private variable def call(self): # public method print( "Ring-ring!" ) def __turn_on(self): # private method self.__how_many_times_turned_on += 1 print( "Times was turned on: ", self.__how_many_times_turned_on ) my_phone = Phone() my_phone.call() print( "The username is ", my_phone.username ) # my_phone.turn_on() # my_phone.__turn_on() # print( “Turned on: “, my_phone.__how_many_times_turned_on) # print( “Turned on: “, my_phone.how_many_times_turned_on) # will produce an error input( "Press Enter to exit" )
Доступ к публичным переменным и методам можно получить из основной программы. Попытка получить приватные данные или запустить приватный метод приведет к ошибке.
Нарушение инкапсуляции
Сам язык предоставляет программисту синтаксический инструмент, который может обойти инкапсуляцию. Читать и изменять частные переменные и вызывать частные функции все же возможно.
#!/usr/bin/python3 class Phone: username = "Kate" # public variable __serial_number = "11.22.33" # private variable __how_many_times_turned_on = 0 # private variable def call(self): # public method print( "Ring-ring!" ) def __turn_on(self): # private method self.__how_many_times_turned_on += 1 print( "Times was turned on: ", self.__how_many_times_turned_on ) my_phone = Phone() my_phone._Phone__turn_on() my_phone._Phone__serial_number = "44.55.66" print( "New serial number is ", my_phone._Phone__serial_number ) input( "Press Enter to exit" )
Несколько слов о Магии
Существуют методы, так называемые «магические методы» («magic methods») или «специальные методы» («special methods»), которые позволяют классам определять свое поведение в отношении стандартных языковых операторов. Примером таких языковых операторов могут служить следующие выражения:
Python 3 поддерживает множество таких методов, полный список можно найти на странице официальной документации языка. __init__ (инициализатор) является наиболее часто используемым из них и запускается при создании нового объекта класса. Другой, __lt__ (расширенное сравнение), определяет правила для сравнения двух объектов пользовательского класса. Такие методы не попадают в категорию «приватных» или «публичных», поскольку служат другим целям и корнями глубоко уходят во внутреннюю структуру языка.
#!/usr/bin/python3 class Phone: def __init__(self, number): # magic method / inititalizer print( "The Phone object was created" ) self.number = number def __lt__(self, other): # magic method / rich comparison return self.number < other.number my_phone = Phone(20) other_phone = Phone(30) if my_phone < other_phone: print( "Two instances of custom class were compared" ) print( "'__lt__' was called implicitly" ) if my_phone.__lt__(other_phone): print( "Now, '__lt__' was used explicitly" ) input( "Press Enter to exit" )
Магические методы могут быть вызваны любым пользователем таким же образом как и любой публичный метод в Питоне, однако они предназначены для неявного использования в своих особых случаях. Специальный случай для метода __init__ — инициализация нового объекта класса. __lt__ служит для сравнения двух объектов.
Заключение
Python3 не обеспечивает ограниченный доступ к каким-либо переменным или методам класса. Данные, которые должны быть скрыты, на самом деле могут быть прочитаны и изменены. В Python3 инкапсуляция является скорее условностью, и программист должен самостоятельно заботиться о ее сохранении.
Источники
- John C. Mitchell, Concepts in programming languages
- Robert C. Martin, Clean Architecture, A Craftsman’s Guide to Software Structure and Design
- OOP concepts
- Encapsulation
- инкапсуляция
Инкапсуляция, наследование, полиморфизм
Недавно мы говорили об основах объектно-ориентированного программирования в python, теперь продолжим эту тему и поговорим о таких понятиях ООП, как инкапсуляция, наследование и полиморфизм.
Инкапсуляция
Инкапсуляция — ограничение доступа к составляющим объект компонентам (методам и переменным). Инкапсуляция делает некоторые из компонент доступными только внутри класса.
Инкапсуляция в Python работает лишь на уровне соглашения между программистами о том, какие атрибуты являются общедоступными, а какие — внутренними.
Одиночное подчеркивание в начале имени атрибута говорит о том, что переменная или метод не предназначен для использования вне методов класса, однако атрибут доступен по этому имени.
Двойное подчеркивание в начале имени атрибута даёт большую защиту: атрибут становится недоступным по этому имени.
File
Однако полностью это не защищает, так как атрибут всё равно остаётся доступным под именем _ИмяКласса__ИмяАтрибута:
Наследование
Наследование подразумевает то, что дочерний класс содержит все атрибуты родительского класса, при этом некоторые из них могут быть переопределены или добавлены в дочернем. Например, мы можем создать свой класс, похожий на словарь:
Для вставки кода на Python в комментарий заключайте его в теги
- Модуль csv - чтение и запись CSV файлов
- Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
- Онлайн-обучение Python: сравнение популярных программ
- Книги о Python
- GUI (графический интерфейс пользователя)
- Курсы Python
- Модули
- Новости мира Python
- NumPy
- Обработка данных
- Основы программирования
- Примеры программ
- Типы данных в Python
- Видео
- Python для Web
- Работа для Python-программистов
- Сделай свой вклад в развитие сайта!
- Самоучитель Python
- Карта сайта
- Отзывы на книги по Python
- Реклама на сайте
Инкапсуляция в Python
Под инкапсуляцией в объектно-ориентированном программировании понимается упаковка данных и методов для их обработки вместе, то есть в классе. В 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)
Попытка выполнить этот код приведет к выбросу исключения:
. print(B.__count) AttributeError: type object 'B' has no attribute '__count'. Did you mean: '_B__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 qty_objects(): return B.__count a = B() b = B() print(B.qty_objects()) # выведет 2
В данном случае метод qty_object() не принимает объект (нет self ), поэтому вызывать его надо через класс. Хотя правильнее такие методы делать статическими (рассматривается в одном из следующих уроков).
Приватными можно делать не только свойства, также методы:
class Natural: def __init__(self, n): self.__origin = n self.number = self.__test() def __test(self): if type(self.__origin) is int and self.__origin > 0: return self.__origin else: print(f"Значение было приобразовано к 1") return 1 a = Natural(34) b = Natural(-250) c = Natural("Hello") print(a.number, b.number, c.number)
Значение -250 было приобразовано к 1 Значение Hello было приобразовано к 1 34 1 1
Может показаться, что несмотря на то, что мы не можем получить значение скрытого атрибута извне, мы можем присвоить ему, обратившись к его имени в той нотации, в которой оно используется в классе (с двумя предстоящими знаками подчеркивания):
class A: def __init__(self, value): self.__field = value a = A(10) # print(a.__field) # Здесь будет ошибка a.__field = 25 print(a.__field) # Будет выведено 25
То есть получается, что при присваивании скрытым полям за пределами класса они становятся открытыми?
На самом деле в данном примере поле экземпляра __field , определенное за пределами класса, – это совсем другое поле. Не тот __field , который находится в классе и обращаться к которому извне надо с помощью _Full__field . В этом можно убедиться, если вывести на экран содержимое атрибута __dict__ :
print(a.__dict__) # Результат: #
Метод __setattr__
В Python атрибуты объекту можно назначать за пределами класса:
class A: def __init__(self, value): self.a = value first = A(10) second = A(25) first.b = "Hello" print(first.__dict__) # print(second.__dict__) #
Если такое поведение нежелательно, его можно запретить с помощью __setattr__ – метода, перегружающего оператор присваивания атрибуту:
class A: def __init__(self, value): self.a = value def __setattr__(self, key, value): if key == 'a': self.__dict__['a'] = value else: raise AttributeError first = A(10) first.b = "Hello"
Traceback (most recent call last): File "test_setattr.py", line 14, in first.b = "Hello" File "test_setattr.py", line 9, in __setattr__ raise AttributeError AttributeError
Метод __setattr__ , если он присутствует в классе, вызывается всегда, когда какому-либо атрибуту выполняется присваивание. Обратите внимание, что присвоение несуществующему атрибуту также обозначает его добавление к объекту.
В примере выше когда создается объект first , в конструктор передается число 10. Здесь для объекта заводится поле a . Попытка присвоения ему значения приводит к автоматическому вызову __setattr__() , в теле которого в данном случае проверяется, соответствует ли имя атрибута строке 'a' . Если это так, то поле с соответствующим ему значением добавляются в словарь атрибутов объекта.
Нельзя в теле __setattr__ написать просто self.a = value , так как это приведет к новому рекурсивному вызову метода __setattr__() . Поэтому поле назначается через словарь __dict__ , который есть у всех объектов, и в котором хранятся их атрибуты со значениями.
Если параметр key не соответствует допустимым полям, то искусственно возбуждается исключение AttributeError . Мы это видим, когда в основной ветке пытаемся обзавестись полем b .
Если объект содержит скрытые поля и к ним происходит обращение из __setattr__ , то делать это надо так, как будто обращение происходит не из класса.
class A: def __init__(self, n): self.a = n self.__x = 100 - n def __setattr__(self, attr, value): if attr in ('a', "_A__x"): self.__dict__[attr] = value else: raise AttributeError a = A(5)
В методе __setattr__ параметр attr – это имя свойства экземпляра в том виде, в котором оно находится в словаре __dict__ . Если свойство скрытое, то в __dict__ оно будет записано через имя класса.
Практическая работа
Разработайте класс с полной инкапсуляцией, доступ к атрибутам которого и изменение данных реализуются через вызовы методов. В объектно-ориентированном программировании принято имена методов для извлечения данных начинать со слова get (взять), а имена методов, в которых свойствам присваиваются значения, – со слова set (установить). Например, get_field , set_field .
Курс с примерами решений практических работ:
pdf-версия
X Скрыть Наверх
Объектно-ориентированное программирование на Python
Что такое инкапсуляция в python
По умолчанию атрибуты в классах являются общедоступными, а это значит, что из любого места программы мы можем получить атрибут объекта и изменить его. Например:
class Person: def __init__(self, name): self.name = name # устанавливаем имя self.age = 1 # устанавливаем возраст def display_info(self): print(f"Имя: \tВозраст: ") tom = Person("Tom") tom.name = "Человек-паук" # изменяем атрибут name tom.age = -129 # изменяем атрибут age tom.display_info() # Имя: Человек-паук Возраст: -129
Но в данном случае мы можем, к примеру, присвоить возрасту или имени человека некорректное значение, например, указать отрицательный возраст. Подобное поведение нежелательно, поэтому встает вопрос о контроле за доступом к атрибутам объекта.
С данной проблемой тесно связано понятие инкапсуляции. Инкапсуляция является фундаментальной концепцией объектно-ориентированного программирования. Она предотвращает прямой доступ к атрибутам объект из вызывающего кода.
Касательно инкапсуляции непосредственно в языке программирования Python скрыть атрибуты класса можно сделав их приватными или закрытыми и ограничив доступ к ним через специальные методы, которые еще называются свойствами .
Изменим выше определенный класс, определив в нем свойства:
class Person: def __init__(self, name): self.__name = name # устанавливаем имя self.__age = 1 # устанавливаем возраст def set_age(self, age): if 1 < age < 110: self.__age = age else: print("Недопустимый возраст") def get_age(self): return self.__age def get_name(self): return self.__name def display_info(self): print(f"Имя: \tВозраст: ") tom = Person("Tom") tom.display_info() # Имя: Tom Возраст: 1 tom.set_age(-3486) # Недопустимый возраст tom.set_age(25) tom.display_info() # Имя: Tom Возраст: 25
Для создания приватного атрибута в начале его наименования ставится двойной прочерк: self.__name . К такому атрибуту мы сможем обратиться только из того же класса. Но не сможем обратиться вне этого класса. Например, присвоение значения этому атрибуту ничего не даст:
tom.__age = 43
Потому что в данном случае просто определяется динамически новый атрибут __age, но это он не имеет ничего общего с атрибутом self.__age .
А попытка получить его значение приведет к ошибке выполнения (если ранее не была определена переменная __age):
print(tom.__age)
Однако все же нам может потребоваться устанавливать возраст пользователя из вне. Для этого создаются свойства. Используя одно свойство, мы можем получить значение атрибута:
def get_age(self): return self.__age
Данный метод еще часто называют геттер или аксессор.
Для изменения возраста определено другое свойство:
def set_age(self, age): if 1 < age < 110: self.__age = age else: print("Недопустимый возраст")
Данный метод еще называют сеттер или мьютейтор (mutator). Здесь мы уже можем решить в зависимости от условий, надо ли переустанавливать возраст.
Необязательно создавать для каждого приватного атрибута подобную пару свойств. Так, в примере выше имя человека мы можем установить только из конструктора. А для получение определен метод get_name.
Аннотации свойств
Выше мы рассмотрели, как создавать свойства. Но Python имеет также еще один - более элегантный способ определения свойств. Этот способ предполагает использование аннотаций, которые предваряются символом @.
Для создания свойства-геттера над свойством ставится аннотация @property .
Для создания свойства-сеттера над свойством устанавливается аннотация имя_свойства_геттера.setter .
Перепишем класс Person с использованием аннотаций:
class Person: def __init__(self, name): self.__name = name # устанавливаем имя self.__age = 1 # устанавливаем возраст @property def age(self): return self.__age @age.setter def age(self, age): if 1 < age < 110: self.__age = age else: print("Недопустимый возраст") @property def name(self): return self.__name def display_info(self): print(f"Имя: \tВозраст: ") tom = Person("Tom") tom.display_info() # Имя: Tom Возраст: 1 tom.age = -3486 # Недопустимый возраст print(tom.age) # 1 tom.age = 36 tom.display_info() # Имя: Tom Возраст: 36
Во-первых, стоит обратить внимание, что свойство-сеттер определяется после свойства-геттера.
Во-вторых, и сеттер, и геттер называются одинаково - age. И поскольку геттер называется age, то над сеттером устанавливается аннотация @age.setter .
После этого, что к геттеру, что к сеттеру, мы обращаемся через выражение tom.age .