На этом шаге мы рассмотрим его назначение и особенности использования.
Шаблоны именования, которые мы рассмотрели к этому моменту, получают свой смысл только из согласованной договоренности. В случае атрибутов (переменных и методов) класса Python, которые начинаются с двойных символов подчеркивания, все немного по-другому.
Префикс, состоящий из двойного символа подчеркивания, заставляет интерпретатор Python переписывать имя атрибута для того, чтобы в подклассах избежать конфликтов из-за совпадения имен.
Такое переписывание также называется искажением имени (name mangling) - интерпретатор преобразует имя переменной таким образом, что становится сложнее создать конфликты, когда позже класс будет расширен.
Звучит довольно абстрактно. Ниже приводится небольшой пример кода, который мы сможем использовать для экспериментирования:
>>> class Test: def __init__(self): self.foo = 11 self._bar = 23 self.__baz = 42
Давайте взглянем на атрибуты объекта, использовав встроенную функцию dir():
>>> t = Test() >>> dir(t) ['_Test__baz', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_bar', 'foo']
Результат показывает список с атрибутами объекта. Давайте возьмем этот список и отыщем наши первоначальные имена переменных foo, _bar, и __baz. Обещаю, вы обнаружите несколько интересных изменений.
Прежде всего, в списке атрибутов переменная self.foo появляется неизмененной как foo.
Далее, self._bar ведет себя таким же образом - она обнаруживается в классе как _bar. Как уже было отмечено, в данном случае начальный символ подчеркивания - это просто договоренность, подсказка программисту.
Однако с атрибутом self.__baz все выглядит немного по-другому. Когда вы попытаетесь отыскать в списке атрибут __baz, вы увидите, что переменной с таким именем там нет.
Так что же произошло с __baz?
Если вы приглядитесь, то увидите, что в этом объекте имеется атрибут с именем _Test__baz. Это и есть искажение имени, которое применяет интерпретатор Python. Это делается, чтобы защитить переменную от переопределения в подклассах.
Давайте создадим еще один класс, который расширяет класс Test и пытается переопределить его существующие атрибуты, добавленные в конструкторе:
>>> class ExtendedTest(Test): def __init__(self): super().__init__() self.foo = 'переопределено' self._bar = 'переопределено' self.__baz = 'переопределено'
Итак, какими, по вашему мнению, будут значения foo, _bar и __baz в экземплярах класса ExtendedTest? Давайте посмотрим:
>>> t2 = ExtendedTest() >>> t2.foo 'переопределено' >>> t2._bar 'переопределено' >>> t2.__baz Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> t2.__baz AttributeError: 'ExtendedTest' object has no attribute '__baz'
Постойте, почему при попытке проверить значение t2.__baz мы получаем исключение AttributeError? Оказывается, что этот объект вообще не имеет атрибута __baz:
>>> dir(t2) ['_ExtendedTest__baz', '_Test__baz', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_bar', 'foo']
Как видите, имя __baz превратилось в _ExtendedTest__baz, чтобы предотвратить случайное изменение. Но первоначальное имя _Test__baz по-прежнему на месте:
>>> t2._ExtendedTest__baz 'переопределено' >>> t2._Test__baz 42
Искажение имени с двойным символом подчеркивания для программиста совершенно очевидно. Взгляните на следующий пример, который это подтверждает:
>>> class ManglingTest: def __init__(self): self.__mangled = 'Привет' def get_mangled(self): return self.__mangled >>> ManglingTest().get_mangled() 'Привет' >>> ManglingTest().__mangled Traceback (most recent call last): File "<pyshell#3>", line 1, in <module> ManglingTest().__mangled AttributeError: 'ManglingTest' object has no attribute '__mangled'
Распространяется ли искажение на имена методов? Конечно! Искажение имен затрагивает все имена, которые в контексте класса начинаются с двух символов подчеркивания (или "дандеров"):
>>> class MangledMethod: def __method(self): return 42 def call_it(self): return self.__method() >>> MangledMethod().__method() Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> MangledMethod().__method() AttributeError: 'MangledMethod' object has no attribute '__method' >>> MangledMethod().call_it() 42
Вот еще один, пожалуй, вызывающий удивление, пример искажения имен в действии:
>>> _MangledGlobal__mangled = 23 >>> class MangledGlobal: def test(self): return __mangled >>> MangledGlobal().test() 23
В этом примере мы определили глобальную переменную _MangledGlobal__mangled. Затем к этой переменной обратились в контексте класса MangledGlobal. Из-за искажения имен мы смогли сослаться на глобальную переменную _MangledGlobal__mangled просто как на __mangled внутри метода test() класса.
Интерпретатор Python автоматически расширил имя __mangled до _MangledGlobal__mangled, потому что оно начинается с двух символов подчеркивания. Это показывает, что искажение имен точно не связано с атрибутами класса. Оно относится к любому имени, начинающемуся с двух символов подчеркивания, которое используется в контексте класса.
Для программиста иногда самым важным навыком является умение "распознавать шаблоны" (образы, паттерны) и понимать, где их нужно искать. Если в этом месте вы чувствуете себя несколько подавленными, не волнуйтесь. Отдохните и поэкспериментируйте с несколькими примерами из этого шага.
Пусть эти принципы впитаются как следует, чтобы вы наконец осознали общую идею искажения имен и некоторые другие формы поведения, которые здесь упоминались. Если однажды вы столкнетесь с ними "в полях", то хотя бы будете знать, что именно искать в документации.
Если вы слышали разговор опытных питонистов о Python или присутствовали при обсуждении на конференциях, то, возможно, слышали термин дандер (dunder). Вам интересно, что же это такое? Ладно, вот ответ.
В сообществе Python двойные символы подчеркивания часто называют "дандерами" (dunders - это сокращение от англ. double underscores). Причина в том, что в исходном коде Python двойные символы подчеркивания встречаются довольно часто, и, чтобы не изнурять свои жевательные мышцы, питонисты нередко сокращают термин "двойное подчеркивание", сводя его до "дандера".
Например, переменная __baz будет произноситься как "дандер baz". Аналогичным образом, метод __init__() звучит как "дандер init", хотя будет логичным предположить, что так: "дандер init дандер".
Но это всего лишь еще одна из причуд среди прочих согласованных правил именования. Для разработчиков Python это все равно что секретное рукопожатие.
На следующем шаге мы рассмотрим двойной начальный и замыкающий символ подчеркивания.