Знак подчеркивания “_”  —  не изобретение Python, другие языки программирования также используют символ “_” в своих сценариях; однако в семантике языка Python знак подчеркивания фигурирует особенно часто: первое, с чем сталкивается новичок в изучении объектно-ориентированного программирования на Python  —  это метод конструктора объекта __init__, у которого в сигнатуре аж четыре подчеркивания!

По мере изучения и практики языка Python для вас открывается все больше возможностей с помощью банального использования символа нижнего подчеркивания улучшить надежность своего кода, либо даже привнести некоторый новый функционал в программу.

В статье перечислены следующие приёмы разностороннего улучшения кода с помощью символа нижнего подчеркивания “_”:

  1. Создание временных переменных на Python.
  2. Улучшение читаемости кода на Python.
  3. Разрешение конфликта имён в программе на Python.
  4. Создание изолированных (внутренних, инкапсулированных, “приватных”) атрибутов для объектов Python.
  5. Создание защищенных от перезаписи атрибутов для объектов Python.
  6. Переопределение магических методов и специальных атрибутов объектов Python.

1. Временная переменная

Первый из приемов использования символа нижнего подчеркивания связан со временной переменной. Когда нет надобности тратить время на придумывание названия для переменной ввиду её низкой значимости для программы, или же когда никак не удаётся понять, какой идентификатор подойдет для переменной лучше всего  —  будет неплохим решением указать для переменной идентификатор, полностью состоящий из одного знака нижнего подчеркивания.

Последний результат REPL

Аббревиатура REPL расшифровывается как “Read Evaluate Print Loop”, что в переводе означает “Чтение Оценка Печать Цикл”. Запуская что-либо в консоли Python, результат работы программы будет записан в REPL в виде переменной. У этой переменной нет имени, и в качестве идентификатора для неё как раз подойдет один знак нижнего подчеркивания.

Ознакомиться с крайне типичным примером получения сохраненных в REPL результатов выполнения программы можно при использовании среды Jupyter Notebook. Предположим, в первой ячейке вашего блокнота инициирован запуск чего-либо  —  это значит, что в последующей ячейке доступ к результату выполнения программного кода первой ячейки можно получить с помощью символа нижнего подчеркивания:

Однако следует помнить, что тип данных None не буферизируется, из-за чего значение переменной с идентификатором в виде одного символа нижнего подчеркивания НЕ заменяется на None, оставаясь равным результату вычислений самой первой ячейки, как в примере ниже.

Анонимная переменная

Кроме идентификатора для временной переменной, одинарным знаком нижнего подчеркивания также называют те переменные, в именовании которых особенным образом нет необходимости. Зачастую подобные переменные используются в программе всего один раз, в чем и причина их именования идентификатором, который вскоре будет переназначен.

for _ in range(10000):
# Сделать что-то 10000 раз
pass

В приведенном выше примере фрагмент кода выполняется 10k раз подряд с помощью цикла for; однако не имеет значение, какая сейчас итерация цикла, а потому в качестве идентификатора для переменной с номером итерации выбран одинарный символ нижнего подчеркивания. 

Более того, большинство из популярных интегрированных средств разработки по умолчанию знакомы с “эмпирическим правилом” идентификатора-подчеркивания, поэтому переменная с таким идентификатором не проверяется на предмет использования в остальном коде программы; и наоборот, при назначении переменной в вышеописанном цикле for уникального идентификатора, некоторые инструменты интегрированной среды разработки могут начать выдавать разработчику предупреждение о том, что он случайно “определил переменную без её использования”.

Универсальный заполнитель

Еще одно распространенное использование символа нижнего подчеркивания в качестве идентификатора временной переменной  —  это создание заполнителя; зачастую такой приём используется для извлечения значений из кортежа, как в примере ниже.

my_tuple = (1, 2)

x, _ = my_tuple
print(x)

Как показано на примере, в качестве значения переменной x присваивается значение первого из элементов кортежа. А вот вторая переменная игнорируется, поскольку идентификатор-подчёркивание без лишних слов даёт понять, что она используется в качестве “заполнителя”.

Также следует упомянуть о малоизвестном трюке, который заключается в совместном использовании символа звездочки с идентификатором-подчеркиванием ради создания переменной-“заполнителя” с несколькими значениями. Предположим, что обозначен кортеж с несколькими элементами, задача  —  получить из него значения первого и последнего элементов:

my_long_tuple = (1, 2, 3, 4, 5)

x, *_, z = my_long_tuple
print('x =', x)
print('z =', z)

Теперь не имеет значения, сколько элементов содержится в кортеже — переменные x и z всегда будут ссылаться на значения первого и последнего из них.


2. Улучшение читаемости кода

Сейчас речь пойдет о гораздо более известном, чем предыдущий, приёме использования символа нижнего подчеркивания; тем не менее мы всё равно рассмотрим его, несмотря на широкую славу, потому что этот маленький трюк в значительной степени улучшает читаемость чисел в программе.

num = 1_000_000
print(num)

Многие программисты разделяют каждые три цифры числа символом нижнего подчеркивания, чтобы прочитать большое число было легче. Эта странность вызвана тем, что синтаксис Python не позволяет писать большие числа через запятую, зато символ подчеркивания в таком случае разрешен.

Кроме того, разрешается буквально поставить знак подчеркивания в любом случайном месте посреди числа без какого-либо влияния на результат выполнения программы:

num = 100_0000
print(num)

3. Разрешение конфликта имён

Символ нижнего подчеркивания также используется многими разработчиками для избегания появления конфликтов в пространствах имён, при том что на уровне интерпретатора для такого использования подчеркивания нет никакого официального функционала. Таким образом, это еще одно “эмпирическое правило” использования символа нижнего подчеркивания в коде.

Например, необходимо определить функцию получения “класса” (разновидности) некоторых продуктов. Если написать код следующим образом, то будет выброшено исключение SyntaxError:

def print_class(class):
print('Class:', class)

Ошибка возникает из-за того, что идентификатор class является одним из зарезервированных ключевых слов в Python. При его использовании Python автоматически начинает считать, что мы хотим определить класс объектов, но синтаксис для такой операции, очевидно, неверен.

Чтобы избежать использования зарезервированного ключевого слова в качестве идентификатора, но сохранить при этом описательный характер имени переменной, как правило, после ключевого слова в идентификаторе ставится знак нижнего подчеркивания, как показано ниже.

def print_class(class_):
    print('Class:', class_)

print_class('Premium')

Конечно, никто не обязан использовать именно слово “class” в качестве идентификатора для переменной, потому что это просто имя переменной; однако иногда именно такое название существенно улучшает читаемость кода, в таком случае не хочется  —  и не нужно, как мы узнали только что  —  избегать использования зарезервированных ключевых слов.

4. Изолированный атрибут объекта

Символ нижнего подчеркивания также используется для защиты некоторых атрибутов и методов в том случае, когда мы хотим оградить атрибут от использования вне объекта, которому он принадлежит.

Защита от импорта

Например, в файле с расширением .py пределены ряд констант и функций. После сохранения данного файла, любой программист может попробовать импортировать его в качестве модуля в другой файл программы; однако может статься так, что оный Python файл содержит исключительно локальные вспомогательные функции, которые не должны быть импортированы для использования в другой среде.

В таком случае проблему решает добавление знака нижнего подчеркивания перед константами или функциями: теперь они не будут импортироваться при выполнении команды from … import *.

Давайте создадим файл lib.py и внутри него определим следующие две функции:

def test_func():
    print("I'm a test")

def _test_func():
    print("I'm a _test")

Далее, в другом файле, выполним импорт всего содержимого из нашего модуля lib.py:

from lib import *

Теперь легко проверить, что первая test_func() была успешно импортирована, чего нельзя сказать о функции _test_func()!

Однако, пожалуйста, воспринимайте вышеописанный приём как “удобное средство”, предотвращающее импорт ненужных функций. Данный трюк НЕ помешает другому программисту явно импортировать куда угодно всё, что он захочет.

Явный импорт продолжает работать:

from lib import _test_func

5. Защищенные атрибуты класса

Символ нижнего подчеркивания помогает организовать защиту не только от лишнего импорта ненужных функций, но и от вызова извне сугубо внутренних атрибутов и методов класса.

Для защиты атрибутов класса от вызова извне нужно добавить двойное подчеркивание __ в самом начале идентификаторов тех атрибутов или методов, которые необходимо защитить от перезаписи.

Для примера мы определим класс под названием Person, а затем создадим его экземпляр:

class Person():
def __init__(self, name, age):
self.name = name
self.__age = age
def show(self):
print(f"Меня зовут {self.name} и мне {self.__age} лет")
def __show(self):
print(f"Привет {self.name}, ты меня вызвал!")
p1 = Person('Chris', 33)

Таким образом, для вывода на экран заранее заданной строки идеально подойдёт метод show():

Тем не менее, если попытаться получить доступ к значению атрибута __name или вызвать метод __show(), которые “защищены” двойными подчеркиваниями в самом начале их идентификаторов, то произойдёт выброс исключения, и программа прекратит работу.

Опять же, эта “защита” не прописана на уровне интерпретатора, она удобна лишь тогда, когда хочется скрыть от вызова какой-то внутренний функционал класса, потому что всегда можно прибегнуть к явному вызову атрибута или метода: просто добавьте перед ними указание _<имя_класса>:

6. Специальные атрибуты и магические методы

Про магические методы знает каждый, но данный аспект настолько важен, что его нельзя пропускать так или иначе. Метод-конструктор __init__(), который использовался в одном из первых примеров статьи, как раз таки относится к магическим методам, в документации языка программирования Python называемым “специальные атрибуты”. Кроме метода-конструктора, одним из самых важных магических методов каждого класса можно назвать __repr__(), определяющий, что должно быть выведено при запросе строкового представления объекта. Если вы знакомы с Java, то легко узнаете в методе __repr__() примерный аналог toString().

Для примера определим класс Person и переопределим его метод __repr__(), чтобы настроить вывод на экран при передаче функции print() экземпляра данного класса.

В первом примере метод __repr__() не переопределён, в следствии чего функция print() просто сообщает нам имя класса и его адрес в памяти компьютера, что не очень полезно для отладки:

class Person():
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person('Chris', 33)
print(p1)

Теперь давайте переопределим функцию __repr__() и попробуем снова:

def __repr__(self):
return f"Меня зовут {self.name} и мне {self.age} лет."

Вывод

В статье представлены несколько приёмов, связанных с использованием символа нижнего подчеркивания в языке программирования Python. Некоторые из них позволяют получить дополнительные возможности в программировании, другие  —  улучшают читабельность и надежность кода программы. Кроме того, в статье рассмотрены способ защиты от импорта объектов, необходимых только для организации локальной работы одного модуля и способ защиты от перезаписи атрибутов/методов классов.

К вышеописанным советам стоит прислушаться хотя бы потому, что они ускорят ваш процесс понимания кода других Python-программистов.

Жизнь коротка, используйте Python!


Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Christopher Tao: Why Python Loves Underscores So Much

Предыдущая статьяПоэтапное создание рабочей контактной формы в Next.js
Следующая статьяСайты для генерации верстки HTML/CSS, которые ускоряют разработку адаптивных интерфейсов