Как реализуются приватные и защищённые методы в Python
Что проверяют
Знаете ли вы, что в Python нет модификаторов доступа, а есть соглашения и один механизм переименования. Типичный уточняющий вопрос: как вызвать «приватный» метод снаружи и зачем тогда вообще нужно двойное подчёркивание.
Как отвечать
- одно подчёркивание
_method: соглашение «внутреннее, не трогайте». Интерпретатор ничего не запрещает, но линтеры и IDE предупредят, аfrom module import *такие имена не импортирует, если в модуле не задан__all__; - два подчёркивания
__method: включается name mangling. Внутри классаAccountимя превращается в_Account__method, поэтомуobj.__method()снаружи даётAttributeError, ноobj._Account__method()спокойно работает; - dunder-имена
__init__,__len__: с подчёркиваниями с двух сторон переименования нет, это специальные методы протоколов, и свои имена такого вида придумывать не стоит.
Главное назначение mangling не защита, а избежание конфликтов имён при наследовании. Если базовый класс и подкласс оба заведут __cache, получатся _Base__cache и _Child__cache, и они не перетрут друг друга. Для обычной «приватности» сообщество советует одно подчёркивание, это прямо сказано в PEP 8.
Когда нужен контролируемый доступ к атрибуту, берут @property с сеттером: снаружи это по-прежнему атрибут, а проверка значения живёт в методе. Так можно начать с публичного поля и позже добавить валидацию, не меняя вызывающий код.
Хороший финал ответа: в Python инкапсуляция держится на договорённости и код-ревью, а не на запретах интерпретатора. Фраза «we are all consenting adults» как раз про это.
Где спотыкаются
Говорят, что двойное подчёркивание делает метод недоступным извне. Это лишь переименование, и интервьюер тут же попросит вызвать метод через _ClassName__method.
Переносят понятия из Java: «protected доступен только наследникам». В Python _method доступен всем, это договорённость, которую держат линтер и ревью.
Ставят двойное подчёркивание везде «для надёжности», а потом не могут переопределить метод в подклассе или замокать его в тесте: mock.patch.object ищет исходное имя, а в классе лежит уже переименованное.