Подчеркивание в Python

Знак подчеркивания _ или underscore занимает особое место в Python.

Underscore code – код символа подчеркивания

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

  1. змеиный_регистр (snake_case)
  2. имена магических методов и переменных
  3. «приватные» члены класса и коверкание имен (mangling)
  4. игнорирование значения переменной
  5. разделение разрядов в числах
  6. избегание конфликтов с ключевыми словами
  7. хранение последнего результата в интерпретаторе

Поехали от самого известного к необычному!

Змеиный регистр

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

foo_bar = 10
def my_function_to_do_something_special(arg_1, arg_2):
    ...

# не принято писать так:
FooBar = 10
carSpeed = 60
Dont_Do_Like_This()

Магические имена

Опять же, думаю, все видели, что имена магических методов и магических переменных начинаются и заканчиваются в двух знаком подчеркивания (__init__). Вот, например, так:

class CrazyNumber:
    __slots__ = ('n',)
    def __init__(self, n):
        self.n = n
    def __add__(self, other):
        return self.n - other
    def __sub__(self, other):
        return self.n + other
    def __str__(self):
        return str(self.n)

Конфликт с ключевым словом

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

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

Tkinter.Toplevel(master, class_='ClassName')

Но! Если вы пишите классовый метод, принято первый аргумент называть cls, а не class_.

Приватные члены

Приватные члены – это такие, которые предполагаются только для внутреннего использования классом или модулем. Они не должны использоваться из-вне, хотя Python и не запрещает это делать. Есть способы получить доступ к любым приватным вещам, если очень нужно.

Разделение на приватные и публичные члены – это механизм сокрытия (пожалуйста, не путайте с инкапсуляцией). Я писал про разницу между ними в статье «Сокрытие в Python». Здесь кратко напомню.

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

# приватные переменные в модуле
_internal_variable = 'some secret'
_my_version = '1.6'

# приватная функция модуля
def _private_func():
    ...

# приватный класс модуля
class _Base:
    # приватная переменная класса
    _hidden_multiplier = 1.2
    def __init__(price):
        # приватное поле экземпляра класса
        self._price = price * self._hidden_multiplier

⚠️ Влияние на поведение: from module import * не будет импортировать приватные члены модуля. Но можно импортировать их принудительно: from module import _Base, _my_version

Еще приватнее или name mangling

⚠️ Если мы будем использовать не одно, а целых два подчеркивания перед именем, то это задействует механизм name mangling. На русский это можно перевести как «коверкание имени». Python исковеркает данное имя, чтобы избежать конфликтов имен атрибутов между классами в иерархии наследования. Естественно, внутри класса, где определен атрибут с двойным подчеркиванием спереди, он будет доступен также по своему имени. Но на самом деле к имени добавится префикс _ClassName. Проиллюстрирую правило манглинга на примере. Допустим есть класс Tree, и вы пишите метод __rebalance, то его имя превратится в _Tree__rebalance при доступе из-вне класса. Пример кода:

class Tree:
    def __rebalance(self):
        print('Tree.__rebalance')

    def public_method(self):
        # метод доступен по своему имени
        self.__rebalance()

class BinaryTree(Tree):
    # этот метод не перекроет __rebalance из Tree!
    def __rebalance(self):
        print('BinaryTree.__rebalance')

tree = Tree()
tree._Tree__rebalance()  # Tree.__rebalance

btree = BinaryTree()
btree._Tree__rebalance()  # Tree.__rebalance
btree._BinaryTree__rebalance()  # BinaryTree.__rebalance

Кстати, на слэнге двойное подчеркивание называется dunder. Добавление третьего и четвертого подчеркиваний к дополнительным эффектам не приведет!

Игнорирование

Если вам не нужно значение переменной, назовите его просто подчеркиванием.

# просто повтор 10 раз, а счетчик не нужен
for _ in range(10):
    print('Hello')

Так же при распаковке коллекций в переменные вы можете применить сколько угодно подчеркиваний.

def tup():
    return 1, 2, 3, 4

# третье не нужно 
a, b, _, d = tup()
print(a, b, d)  # 1 2 4

# второе и четвертое не нужны
a, _, c, _ = tup()
print(a, c)  # 1 3

# только первое
a, *_ = tup()
print(a)  # 1

# первое и последние
a, *_, d = tup()
print(a, d)  # 1 4

# нужны только 2 последних
*_, c, d = tup()
print(c, d)  # 3 4

Примечание: использовать значение _ в принципе можно (в нем будет последний присвоенный результат), но зачем?

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

# нужен только x 
def get_only_x(x, _y, _z):
    return x

# так нельзя!
def get_only_x(x, _, _):
    return x

Разделение разрядов в числах

Фишка добавлена в Python 3.6. Можно разделять разряды в длинных числах для облегчения чтения кода.

>>> 1_000_000
1000000

>>> 0b1011_1100_0000_1111
48143

>>> 0x_ee12_3b5f
3994172255

>>> 0o_1_2_3_4_5_6_7  # можно хоть каждый разряд отделить!
342391

>>> 10_20_30_40
10203040

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

⚠️ Лично я не знал, про эту фишку, пока не стал писать эту статью. А между тем, она супер удобна, если вы используете интерпретатор Python как калькулятор:

>>> 10 + 20
30
>>> _ + 3
33
>>> _ * 3 + _ * 2
165

# print возвращает None, но None не затирает _ !
>>> print('hello')
hello
>>> None
>>> _
165

Может, я что-то упустил? Если да, присылайте мне в Телеграм!

😈 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈