Подчеркивание в 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 👈 

Многоликий else

Все знают, что ключевое слово else служит для выполнения альтернативной ветки кода, если условие if не выполнилось:

x = 5
if x < 3:
    print("x < 3")
else:
    print("x >= 3")

Но знали ли вы, что есть еще два примения else?

1. for/else, while/else

Если поставить else после тела цикла, то код по else будет выполнен только в том случае, если цикл завершился «нормально», т.е. в цикле не исполнилось break. Пример:

stack = [1, 3, 5, 7]
while stack:
    if stack.pop() == 4:
        break
else:
    print('not found!')

Пример. Простые числа и множители:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            # есть делитель, уходим
            print(n, '=', x, '*', n/x)
            break
    else:
        # цикл не нашел делителей
        print(n, 'простое число!')
2. try/else

В блоке try код else выполняется только в том случае, если не возникло исключений. else можно написать только после блока except, без него – нельзя. Порядок выполнения кода соответствует порядку написания сверху вниз: tryexcept или elsefinally.

try:
    ...
except Exception:
    print('Exception!')
else:
    print('Ok!')
finally:
    print('Bye!')

Причем else не будет также вызван, если сработавшее исключение не подпало под перечисленные except и не было обработано.

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

А вы знали про hash(-1)?

(Речь идет о реализации CPython)

Встроенная функция hash возвращает целое число – хэш-сумму, которое используется при сравнении ключей словаря во время поиска, например. Для пользовательских классов hash вызывает магический метод класса  __hash__ , а для примитивных типов уже есть встроенная реализация на Си. 

Примечательно, что для чисел hash обычно возвращает само же значение числа-аргумента, кроме нескольких случаев. Запустим этот код:

def print_hash(x):
    print(f'hash({x}) = {hash(x)}')
for i in range(2, -4, -1):
    print_hash(i)

Вывод:

hash(2) = 2
hash(1) = 1
hash(0) = 0
hash(-1) = -2  <-- что?
hash(-2) = -2
hash(-3) = -3

Оказывается hash не возвращает -1, а конвертирует его явно в -2. Я изучил исходный код на Си и нашел это место. «Легенда гласит», что в CPython число -1 зарезервировано внутренне для индикации ошибок при выполнении этой функции.

Еще интереснее для рациональных чисел. От hash от NAN – ноль. Плюс еще пасхалка: hash от бесконечности возращает первые несколько цифр числа π. 

print_hash(-1.0)  # -2
print_hash(float('nan'))  # 0
print_hash(float('+inf'))  # 314159
print_hash(float('-inf'))  # -314159

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

Множества в Python

Множество (англ. «set«) – неупорядоченная коллекция из уникальных (неповторяющихся) элементов. Элементы множества в Python должны быть немутабельны (неизменяемы), хотя само содержимое множества может меняться: можно добавлять и удалять элементы из множества.

О неизменяемых множествах написано в конце этой статьи.

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

Создание множества

Сформировать множество можно несколькими способами. Самый простой – перечислить элементы через запятую внутри фигурных скобок {}. Множество может содержать элементы разных типов, главное, чтобы они были неизменяемы. Поэтому кортеж можно поместить в множество, а список – нельзя.

>>> my_set = {1, 2, 3, 4}

>>> my_hetero_set = {"abc", 3.14, (10, 20)}  # можно с кортежем

>>> my_invalid_set = {"abc", 3.14, [10, 20]}  # нельзя со списком
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Можно также воспользоваться встроенной функцией set, чтобы создать множество из другой коллекции: списка, кортежа или словаря. Если это будет словарь – то новое множество будет составлено только из ключей этого словаря. Можно создать множество даже из строки: будет добавлена каждая буква (но только один раз):

>>> my_set2 = set([11, 22, 33])
>>> my_set2
{33, 11, 22}

>>> my_set3 = set((1, 2, 3))
>>> my_set3
{1, 2, 3}

>>> my_set4 = set({"a": 10, "b": 20})
>>> my_set4
{'b', 'a'}

>>> my_set5 = set("hello")
>>> my_set5
{'h', 'l', 'e', 'o'}

Как создать пустое множество? {} – вернет нам пустой словарик, а не множество. Поэтому, нужно использовать set() без аргументов.

>>> is_it_a_set = {}
>>> type(is_it_a_set)
<class 'dict'>

>>> this_is_a_set = set()
>>> type(this_is_a_set)
<class 'set'>

Изменение множеств

Множества можно менять, добавляя или удаляя элементы. Так как они не упорядочены, то индексирование не имеет смысла и не поддерживается: мы не может получать доступ к элементам множества по индексу, как мы это делаем для списков и кортежей.

Добавление одного элемента выполняется методом add(). Нескольких элементов из коллекции или нескольких коллекций – методом update():

>>> my_set = {44, 55}
>>> my_set.add(50)
>>> my_set
{50, 44, 55}

>>> my_set.update([1, 2, 3])
>>> my_set
{1, 2, 3, 44, 50, 55}

>>> my_set.update([2, 3, 6], {1, 50, 60}) 
>>> my_set
{1, 2, 3, 6, 44, 50, 55, 60}

>>> my_set.update("string")
>>> my_set
{1, 2, 3, 6, 'i', 44, 'r', 50, 's', 55, 'n', 'g', 60, 't'}

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

Удаление элементов из множества

Для удаления элемента существуют методы discard() и remove(). Делают они одно и тоже, но если удаляемого элемента нет во множестве, то discard() оставит множество неизменным молча, а remove() – бросит исключение:

>>> my_set = {1, 2, 3, 4, 5, 6}
>>> my_set.discard(2)
>>> my_set
{1, 3, 4, 5, 6}

>>> my_set.remove(4)
>>> my_set
{1, 3, 5, 6}

>>> my_set.discard(10)
>>> my_set
{1, 3, 5, 6}

>>> my_set.remove(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 10

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

>>> my_set = {3, 4, 5, 6, 1, 2}
>>> my_set
{1, 2, 3, 4, 5, 6}
>>> my_set.pop()
1
>>> my_set
{2, 3, 4, 5, 6}

Наконец, очистить множество (т.е. удалить все его элементы) можно методом clear():

>>> my_set = {1, 2, 3}
>>> my_set.clear()
>>> my_set
set()

Проверка членства

Узнать есть ли элемент в множестве очень легко оператором in (или not in, если хотим убедиться в отсутствии элемента):

>>> s = {"banana", "apple"}
>>> "banana" in s
True
>>> "tomato" not in s
True

Таким образом проверяется членства одного элемента, если нужно узнать является ли одно множество подмножеством другого, то оператор in тут не подойдет:

>>> {1, 2} in {1, 2, 3}
False

Тут подойдут операторы < и >. Чтобы получить True, с «широкой» стороны оператора должно стоять множество, полностью содержащее множество, стоящее по «узкую» сторону галочки:

>>> {1, 2} < {1, 2, 3, 4}
True
>>> {5, 6, 7, 8} > {5, 8}
True
>>> {1, 2, 3} < {1, 2, 4}
False

Итерация множеств

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

my_set = {"Moscow", "Paris", "London"}
for elem in my_set:
    print(elem)
Moscow
London
Paris

Операции над множествами

Самое интересное – проводить математические операции над множествами.

Рассмотрим два множества A и B:

A = {1, 2, 3, 4, 5}
B = {4, 5, 6, 7, 8}

Объединение

Объединение множеств – множество, в котором есть все элементы одного и другого множеств. Это коммуникативная операция (от перемены мест ничего не меняется).

В Python используется либо метод union(), либо оператор вертикальная черта «|»:

>>> A = {1, 2, 3, 4, 5}
>>> B = {4, 5, 6, 7, 8}

>>> A | B
{1, 2, 3, 4, 5, 6, 7, 8}

>>> A.union(B)
{1, 2, 3, 4, 5, 6, 7, 8}

>>> B.union(A)
{1, 2, 3, 4, 5, 6, 7, 8}

Пересечение множеств

Пересечение множеств – множество, в которое входят только общие элементы, то есть которые есть и в первом, и во втором множестве. Также коммуникативная операция.

Пересечение вычисляют методом intersection() или оператором амперсандом «&»:

>>> A = {1, 2, 3, 4, 5}
>>> B = {4, 5, 6, 7, 8}

>>> A & B
{4, 5}

>>> B & A
{4, 5}

>>> A.intersection(B)
{4, 5}

Разность множеств

Разность множеств A и В – множество элементов из A, которых нет в B. Не коммуникативная операция!

Выполняется знаком минус «-» или оператором difference():

>>> A = {1, 2, 3, 4, 5}
>>> B = {4, 5, 6, 7, 8}

>>> A - B
{1, 2, 3}

>>> B - A
{8, 6, 7}

>>> A.difference(B)
{1, 2, 3}

>>> B.difference(A)
{8, 6, 7}

Как видно есть разница, в каком порядке идут операнды.

Симметричная разность

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

Используется метод symmetric_difference() или оператор крышка «^»:

>>> A = {1, 2, 3, 4, 5}
>>> B = {4, 5, 6, 7, 8}

>>> A ^ B
{1, 2, 3, 6, 7, 8}

>>> B ^ A
{1, 2, 3, 6, 7, 8}

>>> A.symmetric_difference(B)
{1, 2, 3, 6, 7, 8}

Обратите внимание на эквивалентность операции определениям, которые я привел в начале этого раздела:

>>> A ^ B == (A - B) | (B - A)   # объединение простых разностей
True

>>> A ^ B == (A | B) - (A & B)   # разность объединения и пересечения
True

Прочее

Ко множествам можно применять стандартные функции all(), any(), enumerate(), len(), max(), min(), sorted(), sum(). Описания их ищите тут.

Прочие методы класса set:

copy() Возвращает копию множества
difference_update(other_set)Удаляет из этого множества все элементы, которые есть во множестве, переданным в аргументе
intersection_update(other_set)Обновляет это множество элементами из пересечения множеств
isdisjoint(other_set)Возвращает True, если множества не пересекаются
issubset(other_set)Возвращает True, если это множество является подмножеством другого
issuperset(other_set)Возвращает True, если это множество является надмножеством другого
symmetric_difference_update(other_set)Добавляет в это множество симметричную разность этого и другого множеств

Замороженное множество

Замороженное множество (frozen set) также является встроенной коллекцией в Python. Обладая характеристиками обычного множества, замороженное множество не может быть изменено после создания (подобно тому, как кортеж является неизменяемой версией списка).

Будучи изменяемыми, обычные множества являются нехешируемыми (unhashable type), а значит не могут применятся как ключи словаря или элементы других множеств.

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

Создаются замороженные множества функцией frozenset(), где аргументом будет другая коллекция. Примеры:

>>> A = frozenset({1, 2, 3})
>>> A
frozenset({1, 2, 3})

>>> B = frozenset(['a', 'b', 'cd'])
>>> B
frozenset({'cd', 'b', 'a'})

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

>>> A = frozenset('hello')
>>> B = frozenset('world')
>>> A | B
frozenset({'o', 'r', 'd', 'e', 'l', 'h', 'w'})
>>> A & B
frozenset({'o', 'l'})
>>> A ^ B
frozenset({'d', 'e', 'h', 'r', 'w'})

Теперь вы знаете много о множествах в Python.

Специально для канала PyWay.