Подчеркивание (символ _) — играет важную роль в языке Python и используется в очень разных ситуациях: от улучшения читаемости и управления приватными атрибутами до особой функциональности при обработке данных и интернационализации.

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

Когда используется подчёркивание

Именование

Пожалуй, самое распространенное и важное использование подчеркивания — это именование. Согласно PEP 8:

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

Имена переменных должны следовать той же конвенции, что и имена функций.

Таким образом, подчеркивание служит разделителем между словами в именах функций и переменных. Эта же конвенция применяется к именам методов и атрибутов экземпляров классов.

Это идиоматическая конвенция именования для Python. Но давайте рассмотрим и другие наиболее значимые соглашения, используемые в разных языках программирования:

  • Camel case (myVariableName): Первое слово пишется в нижнем регистре, а первая буква последующих слов — в верхнем. Применяется в таких языках, как JavaScript, Java, C# и Swift.

  • Pascal case (MyVariableName): Первая буква каждого слова в верхнем регистре. Используется в Python (для имён классов), C#, Pascal, Java и C++.

  • Snake case (my_variable_name): Все слова пишутся в нижнем регистре с подчёркиванием между ними. Применяется в Python (для переменных и имен функций), а также в Ruby.

  • Screaming snake case (MY_VARIABLE_NAME): Все буквы пишутся в верхнем регистре, а слова разделяются подчёркиваниями. Используется в Python для констант, а также в C, C++ и Java.

  • Kebab case (my-variable-name): Слова в нижнем регистре, разделенные дефисами. Применяется в URL и именах классов CSS.

  • Hungarian notation (iCount, strName): Имена переменных содержат префиксы, указывающие на типы или области видимости. Раньше использовалась в C и C++.

Ниже приведены несколько примеров имен переменных на Python, в которых используется подчёркивание:

write_to_database()
read_data()

df_history
df_actual

Символ подчеркивания играет ещё одну роль в именовании в Python. Согласно PEP 8, если нужно создать объект с именем, которое конфликтует с зарезервированным, то можно добавить подчеркивание в конце:

Если имя аргумента функции конфликтует с зарезервированным ключевым словом, обычно лучше добавить одно подчеркивание в конце, чем использовать аббревиатуру или искажённое написание. То есть, class_ лучше, чем clss.

Например, часто используются имена class_ и type_.

Подчеркивание также применяется в именовании констант. Как указано в PEP 8:

Константы обычно определяются на уровне модуля и записываются прописными буквами, а слова разделяются подчеркиванием.

Вот три примера имен констант:

NO_OF_DAYS
SIGNIF_LEVEL
RUN_DEBUGGER

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

Однако, стоит отметить, что классы в Python обычно не используют подчеркивание в именах. Например, вместо того чтобы назвать класс book_publisher, стоит назвать его BookPublisher. Существуют, конечно, хорошо известные исключения, такие как list и dict, но это не значит, что следует создавать подобные исключения самостоятельно.

Dunder- (Double UNDERscore) или магические методы

Эта роль также связана с именованием, но здесь речь идет об внутренних именах в языке Python. Вы часто будете замечать подчёркивания в именах так называемых магических методов. Это специальные методы, которые начинаются и заканчиваются двойными подчеркиваниями (__). Из-за этого двойного подчеркивания их иногда называют dunder-методами, что является сокращением от double underscore.

Dunder-методы играют важную роль в различных элементах синтаксиса языка Python. Двойные подчеркивания в их именах важны, так как они указывают на то, что эти методы особенные. Эта конвенция именования предотвращает перезапись этих встроенных (магических) методов пользовательскими методами.

Вот несколько примеров dunder-методов в Python:

  • __init__: отвечает за создание экземпляров класса.

  • __str__: определяет поведение функций str() и print(), применяемых к объекту.

  • __len__: возвращает длину составного объекта.

  • __getitem__: обеспечивает поддержку индексации.

  • __add__, __mul__ и т.д.: позволяют объектам поддерживать арифметические операции.

Обратите внимание, что нам не стоит использовать dunder-методы напрямую; вместо этого они вызываются интерпретатором Python во время выполнения различных операций. Например, когда мы вызываем len(x), Python внутренне вызывает x.__len__(). Хотя нам не стоит использовать последний метод напрямую, но он будет работать без каких-либо проблем:

x = [1, 2, 3]

print(len(x))
# 3

print(x.__len__())
# 3

Хорошей практикой является не определять новые пользовательские dunder-методы. Однако, вполне допустимо переопределять существующие магические методы в пользовательских классах.

Специальные атрибуты

Хотя методы, начинающиеся и заканчивающиеся двойными подчеркиваниями, называются магическими или dunder-методами, атрибуты, следующие этой конвенции именования, обычно называются специальными атрибутами. Эти атрибуты автоматически создаются и управляются Python и предоставляют информацию об объектах. Вот несколько примеров:

  • __name__: используется модулями, классами, методами классов и функциями для хранения имени объекта.

  • __doc__: хранит строку документации модуля, класса, метода или функции.

  • __file__: используется модулями для хранения пути к файлу, из которого был загружен модуль.

Переменные-заглушки (dummy-переменные)

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

Что-то подобное часто используется в циклах, когда мы не планируем использовать переменную, инициализированную в цикле. Давайте сравним несколько ситуаций:

Используем переменную, инициализированную в цикле:

for i in range(1, 4):
    print(f"number {i}")
    
# 1
# 2
# 3

Не используем переменную, инициализированную в цикле:

for _ in range(3):
    print("Hello, World!")
    
# Hello, World!
# Hello, World!
# Hello, World!

Также хорошей практикой является использование подчеркивания для «захвата» объекта, возвращаемого функцией или методом, в тех случаях, когда этот объект не будет использоваться, как показано ниже:

def save(obj: Any, path: pathlib.Path) -> bool:
    # объект успешно или неуспешно сохраняется
    if not success:
        return False
    return True

_ = save(obj, pathlib.Path("file.csv"))

В этом примере мы присвоили возвращаемое значение функции save() переменной _. Это было сделано, потому что нам не нужно использовать этот объект в коде. Если бы нам понадобилось его использовать, мы бы поступили так:

saved = save(obj, pathlib.Path("file.csv")

Указание приватных методов и атрибутов

В Python нет возможности создать полностью приватные методы или атрибуты.

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

Рассмотрим следующий класс:

class Me:
    def __init__(self, name, smile=":-D"):
        self.name = name
        self.smile = smile
        self._thoughts = []

    def say(self, what):
        return str(what)

    def _think(self, what):
        self._thoughts += what

У нас есть класс Me, который представляет... вас. Можно создать себя, используя следующие атрибуты:

  • .name, публичный атрибут → ваше имя, безусловно, открыто для всех.

  • .smile, публичный атрибут → ваша улыбка видна всем, так что она определенно публична.

  • ._thoughts, приватный атрибут → ваши мысли принадлежат только вам, поэтому они приватны.

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

Рассмотрим два метода:

  • .say(), публичный метод → когда вы что-то говорите, люди могут вас услышать.

  • ._think(), приватный метод → ваши мысли остаются в тайне, и только вы можете их знать. Если вам хочется поделиться ими, используйте публичный метод .say(), но если вы предпочитаете хранить их при себе, применяйте приватный метод ._think().

Можно создать публичный метод, чтобы сказать приватную мысль вслух:

def say_thought(self, which):
        return self._thoughts[which]

Последняя операция в интерактивной сессии

В Python 3 подчеркивание также используется для хранения результата последней операции в сессии интерпретатора. Это может быть полезно для быстрого использования результата предыдущей операции в новой операции, когда результат не был присвоен имени. Например:

>>> 1 + 2
3
>>> _ * 3
9
>>> y = 10
>>> _
9
>>> 100
>>> _
100

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

Форматирование числовых значений

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

x = 1_000_000
print(x)
# 1000000

print(1.009_232_112)
# 1.009232112

print(1_021_232.198_231_111)  
# 1021232.198231111

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

Использование functools.singledispatch

В functools.singledispatch подчеркивание (_) часто используется в качестве имени функции, чтобы указать, что диспетчеризированные функции являются анонимными реализациями, предназначенными для обработки определенных типов. Этот стилистический выбор предполагает, что имя функции не имеет значения; важно то, какой тип обрабатывает функция. Такое использование помогает сохранить пространство имен чистым и подчеркивает, что логика напрямую связана с механизмом singledispatch, а не предназначена для прямых вызовов. Вот пример из PEP 443:

from functools import singledispatch

@singledispatch
def fun(arg, verbose=False):
    if verbose:
        print("Let me just say,", end=" ")
    print(arg)

@fun.register(int)
def _(arg, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)

@fun.register(list)
def _(arg, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, elem in enumerate(arg):
        print(i, elem)
  print(i, elem)

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

Если вы хотите использовать статические проверки на вышеуказанный код, такие как MyPy, будьте готовы к тому, что это может вызвать ошибку из-за того, что вы определяете _ более одного раза. Самое простое решение этой проблемы — добавить комментарий # type: ignore в конце строк, где определяется _. Альтернативно, можно дать обычные имена этим функция, но это будет нетипичным подходом для functools.singledispatch.

Интернационализация и локализация

Интернационализация (часто сокращаемая как i18n) и локализация (сокращаемая как l10n) делают приложения адаптируемыми к различным языкам и регионам без необходимости вносить изменения в код.

В Python эти два процесса могут быть реализованы с помощью модуля gettext, который позволяет приложениям поддерживать несколько языков. В gettext общепринято использовать символ подчеркивания (_) в качестве псевдонима для функции gettext, которая отмечает строки для перевода.

import gettext
import locale

# Устанавливаем русскую локаль:
locale.setlocale(locale.LC_ALL, "ru_RU.UTF-8")

# Устанавливаем путь к файлам перевода .mo
# и выбираем текстовый домен:
gettext.bindtextdomain(
    "myapp",
    "/path/to/my/locale/directory"
)
gettext.textdomain("myapp")

# Подчеркивание обычно используется в качестве псевдонима для gettext.gettext:
_ = gettext.gettext

# _() оборачивает текст для перевода. 
# gettext.gettext() _() — извлекает переведенную строку.
# Использование подчеркивания проще; сравните:
print(_("Hello, World!"))  # Привет, мир!
print(gettext.gettext("Hello, World!"))  # Привет, мир!

Итого: когда в приложении используется gettext, подчеркивание довольно полезно.

Игнорирование значений при распаковке

В Python можно использовать подчеркивание для игнорирования ненужных значений при распаковке составных объектов. Это делает код более чистым и понятным, поскольку позволяет избежать определения переменных, которые не будут использоваться в дальнейшем.

Присвоить ненужные значения подчеркиванию (_), которое действует как переменная-заглушка, можно так:

a, _, b = (1, 2, 3)

print(a)
# 1

print(b)
# 3

В этом примере подчеркивание используется для игнорирования центрального значения (2).

Если же нужно проигнорировать несколько значений, что может быть особенно полезно при работе с длинными последовательностями, то можно воспользоваться звёздочкой с подчеркиванием (*_):

a, *_, b = [1, 2, 3, 4, 5]

print(a)
# 1

print(b)
# 5

Мы присвоили первое и последнее значения списка переменным a и b соответственно. Остальные значения — 2, 3 и 4 — присвоены *_, что означает, что они игнорируются и больше не будут использоваться.

Использование подчеркиваний таким образом улучшает читаемость кода, делая очевидным, что определенные значения не будут использоваться и, следовательно, не требуются. Это помогает сосредоточиться именно на нужных данных. Кроме того, зачем присваивать эти три значения переменным, если они не будут использованы? Это противоречит принципам хорошему стилю кода.

Заключение

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

_ имеет множество различных применений, самые важные из которых мы рассмотрели в этой статье.

?Если тебе интересны и другие полезные материалы по IT и Python, то можешь подписаться на мой канал PythonTalk или посетить сайт OlegTalks?

Комментарии (19)


  1. Sild
    03.01.2025 15:48

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


    1. Dadadam999
      03.01.2025 15:48

      В С# официальный кодстайл подразумевает начинать приватные свойства с нижнего подчёркивания. Это правда удобно. Однако в пайтоне это правда какой-то прикол, как и ООП в целом на пайтоне. ООП на нём не юзабелен от слова совсем.


  1. MountainGoat
    03.01.2025 15:48

    Главного не сказали. Ваши собственные методы могут начинаться как с одного так и с двух подчёркиваний. При этом не важно, как они кончаются. Одно подчёркивание - это просто договор. Вы отмечаете, что метод приватный, но если его вызвать со стороны, то он нормально отработает без проблем. Разве что Pylance вас обматерит, и то, кажется, не по умолчанию. И это хорошо, потому что позволяет без проблем делать юниттесты а так же делать условные брейкпоинты. Я всегда говорил, что в любом языке должна быть возможность нарушать private и автоматически давать по рукам при использовании её в самом коде. Иначе код обрастает геттерами, которые нужны только для отладки.

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


    Другое дело - два подчёркивания. Это попытка накостылить принудительный приват. Увидев метод, начинающийся с двух подчёркиваний, CPython вместо него добавит в имя также имя класса того места, где метод был вызван. То есть в членах того же класса оно совпадёт, а при попытке вызова из другого класса получим "метод не найден". К сожалению, есть много краевых случаев где это работает неочевидным образом, поэтому много разных людей называют эту возможность ошибкой и призывают ей не пользоваться.


    1. Andrey_Solomatin
      03.01.2025 15:48

      Ваши собственные методы могут начинаться как с одного так и с двух подчёркиваний. При этом не важно, как они кончаются.

      Чем заканчиваются важно.

      class Sample:
          def __my__dandr__(self):
              print("Hello World")
      
      if __name__ == '__main__':
          Sample().__my__dandr__()

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

      А можно пример таких крайних случаев?


      1. MountainGoat
        03.01.2025 15:48

        Что‑то связанное с наследованием и исключениями, когда исключение генерируется в непереопределённом методе и обрабатывается в переопределённом. Не запоминал, потому что избегаю как name mangling так и исключений — не люблю неявное.

        Вот ещё популярное видео: https://www.youtube.com/watch?v=0hrEaA3N3lk


        1. Andrey_Solomatin
          03.01.2025 15:48

          Вот ещё популярное видео: https://www.youtube.com/watch?v=0hrEaA3N3lk

          Начал смотреть и бросил.

          Да, существует много людей которые Питон "выучили" сами и не знают спецификаций языка. Это вообщем проблема всех языков.

          Моя рекомнедация новичкам: используйте линтеры, они съэкономят вам время. SLF001 Private member accessed: __count
          Ну и коменты на хабре читать, тут многие интересные темы поднимают.


        1. Andrey_Solomatin
          03.01.2025 15:48

          Не запоминал, потому что избегаю как name mangling так и исключений — не люблю неявное.

          Нейвное, это когда что-то работает не так как выглядит. name mangling вполне явная штука, они чётко описанна в спецификации.

          У name mangling единственная цель это сделать невозможными конфликты переменных при наследовании. Если вы создаёте класс для наследования или наследуетесь и не используете эту защиту, то у вас ненадёжный код. Не делайте так.

          "Избегаю name mangling", звучит двусмылсенно. Я бы сказал избегать наследований это норм, а вот если уже используете наледование, то без приватных членов класса никак нельзя.


    1. posledam
      03.01.2025 15:48

      Речь не о питоне, но не единожды имел проблемы, когда в тестах путём рефлексии обращались к приватным полям. И это значительно мешало рефакторингу. Приватные на то и приватные, что это исключительно особенность реализации. Т.е. разработчик должен иметь полную гарантию, что он может спокойно изменять подробности реализации, удалять, менять, разделять, слеплять, не боясь, что сломаются какие-то тесты только из-за внутреннего рефакторинга, который не меняет поведения. В общем, плохо это, как по мне, и приватный член должен быть действительно приватным членом. Иначе это не защита, а имитация.


      1. rsashka
        03.01.2025 15:48

        Программист в коде может написать все, что угодно, хоть разименовать нулевой адрес (например для написания теста). Соглашения о вызовах и именовании, это именно соглашения, и если кто-то не захотел их использовать (или не смог по другому), это не является доводом, чтобы усложнить жизнь всем остальным, заставляя их использовать перректальные механизмы доступа к приватным полям.

        Возможно, это был единственный способ сделать задачу из-за общей корявой архитектуры приложения (например, нужные для наследников классов данные без геттеров и сеттеров сделали приватными, вместо защищенных)


        1. posledam
          03.01.2025 15:48

          Если программист сам себе программист, то ради бога. Речь о крупных проектах, которые решают сложные задачи, и разрабатываются большими командами. Например, я разрабатываю компонент, а другой разработчик логику с этим компонентом, третий пишет тесты. Пользователи компонента свяжут мне руки, если начнут трогать приватные поля ради каких угодно, хоть самых благих целей. Когда я буду вносить исправления, оптимизацию, не ломая логику компонента, могу ненароком поломать чужие тесты или бизнес-логику. Раз есть риски, то может оказаться, что руки связаны и надо пройти кучу согласований для изменений, которые должны быть естественными и безопасными. Именно поэтому приватные поля называются приватными, это не просто "соглашение", это мера защиты, которая гарантирует разработчику уверенность, что вот эти вещи -- они детали реализации, и никто нигде на это завязываться не будет.

          Другими словами, это как ходить без одежды, следуя соглашению, что никто "туда" не смотрит. Только врач или супруг/супруга может :)

          Но я не настаиваю, просто поделился опытом и мнением.


          1. rsashka
            03.01.2025 15:48

            Я тоже говорю про крупные проекты и полностью разделяю ваше негодование, когда разработчик нарушает те или иные соглашения при написании кода.

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

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


            1. posledam
              03.01.2025 15:48

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

              Не очень понял насчёт возможностей работы с кодом. Если вы определили член, как приватный, вы снижаете возможности? Нет, вы защищаете свою реализацию, чтобы иметь возможность безопасно вносить изменения. Если технически возможность такая есть, значит вы рискуете кому-то испортить день, а то и не день.

              Насчёт гарантий защиты. Вы дверь на ключ закрываете? А в курсе, что это не 100% гарантия защиты от проникновения? Понятно, что нет. Но это нужно приложить усилия, которые очевидным образом кричат о том, что вы делаете что-то плохое. И вам не надо даже знать о соглашениях, это очевидно.


          1. Andrey_Solomatin
            03.01.2025 15:48

            Например, я разрабатываю компонент, а другой разработчик логику с этим компонентом, третий пишет тесты.


            В идеале ваш модуль должен быть покрыт тестами вами. А тесты на того, кто использует ваш модуль не должны вообще ваш трогать.

            Тесты которые пишутся после того как кто-то начал использовать ваш код идея не очень хорошая. Они уже мало что найдут.

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


            1. posledam
              03.01.2025 15:48

              Правильно, тесты пишутся разработчиками. Но не обязательно один и тот же пишет и код и тесты. Во многих языках программирования есть такой уровень доступа к членам модуля/класса, как "дружественный". Т.е. можно определить дружественный модуль и позволить доступ к некоторым членам, обычно это как раз и применяется для тестирования.

              Тестопригодность это один из показателей хорошего дизайна.


  1. Andrey_Solomatin
    03.01.2025 15:48

    Я примерно тоже писал, только про name mangling поподробнее раскрыл.
    https://habr.com/ru/articles/842954/


  1. ENick
    03.01.2025 15:48

    Мне очень удобно смотреть версию библиотеки, например для numpy это будет выглядеть как numpy.__version__


  1. freecom
    03.01.2025 15:48

    помню время когда было всего 640к памяти.


  1. nvaulin
    03.01.2025 15:48

    Я бы еще добавил что подчеркивание можно использовать для более понятной записи чисел: 10000 -> 10_000. С одной стороны мелочь. С другой стороны имхо интересно, т.к. это единственное (да?) место, где знак _ выступает не в качестве части имени объекта (переменной / метода ...)


    1. obulygin Автор
      03.01.2025 15:48

      Об этом есть в разделе "Форматирование числовых значений" :)