Перечисления (Enum)

В Python нет специального синтаксиса для перечислений, зато есть модуль enum и класс Enum в нем, от которого можно отнаследоваться для создания собственного перечисления:

from enum import Enum
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

Задавать переменные этого типа можно несколькими способами:

c = Color(2)     # по значению
c = Color['RED'] # по строковому имени
c = Color.RED    # по члену класса

Значения из Enum человеко-читаемы при печати:

>>> print(Color.RED)
Color.RED

А также:

>>> Color.RED.name
'RED'
>>> Color.RED.value
1

Для сравнения эквивалентности используют оператор is (хотя == и != тоже работают):

if c is Color.RED:
    print('Red!')
if c is not Color.BLUE:
    print('Not blue!')

Для нескольких значений можно использовать in:

if c in (Color.BLUE, Color.GREEN):
    print('No red at all!')

Если неохота задавать значение самостоятельно, можно делать это автоматически:

from enum import Enum, auto
class Numbers(Enum):
    ONE = auto()
    TWO = auto()
    THREE = auto()
    FOUR = auto()

Члены перечислений хэшируемы и могут быть ключами словаря:

apples = {}
apples[Color.RED] = 'sweet'
apples[Color.GREEN] = 'sour'
>>> apples
{<Color.RED: 1>: 'sweet', <Color.GREEN: 2>: 'sour'}

Кратко создать перечислимый тип можно в функциональном стиле:

from enum import Enum
Animal = Enum('Animal', 'ANT BEE CAT DOG')

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

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

Animal = Enum('Animal', 'ANT, BEE, CAT, DOG')
Direction = Enum('Direction', ['NORTH', 'SOUTH', 'WEST', 'EAST'])
Color = Enum('Color', [('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)])
Mood = Enum('Mood', {'HAPPY': ':-)', 'SAD': ':-('})

P.S. К сожалению, не все современные IDE понимают такое определение Enum, даже последний PyCharm ругается на аргументы и не активирует авто-дополнение по вариантам. Надеюсь, в будущем ситуация изменится в лучшую сторону.

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

🎧 Качаем музыку

Вспомнил, что в начале 2000-х качал музыку с http://music.lib.ru. Сайт всегда радовал редкими и интересными композициями. Недавно совершенно случайно вспомнил о сайте, зашел, и о чудо! Он до сих пор работает и с тех пор ничуть не изменился! Ко мне пришла мысль, почему бы не скачать немного музыки и послушать. Чтобы не качать песни по одной, я решил автоматизировать эту задачу и заодно написать для вас этот туториал.

ЧИТАТЬ…

🧞‍♂️ Слабые ссылки

Недавно в заметке про управление памятью в Python мы упоминали слабые ссылки. По опросу на моем канале лишь 1 человек из 4 знал про слабые ссылки в Python, и лишь 6% читателей их применяли. Что же это такое? Слабые ссылки позволяют получать доступ к объекту, как и обычные, однако, так сказать, они не учитываются в механизме подсчета ссылок. Другими словами, слабые ссылки не могут поддерживать объект живым, если на него не осталось больше сильных ссылок.

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

Встроенный модуль weakref отвечает за функциональность слабых ссылок.

📎 Пример. Создаем класс Foo, сильную ссылку на его экземпляр, затем слабую ссылку, проверяем:

import weakref

class Foo: ...

strong_foo = Foo()
weak_foo = weakref.ref(strong_foo)
print(weak_foo())   # вызов слабой ссылки - доступ к исходному объекту
print(weak_foo() is strong_foo)  # True

del strong_foo  # это была последняя сильная ссылка
print(weak_foo())  # None

После того, как мы избавились от единственной сильной ссылки на экземпляр класса, объект уничтожился, а слабая ссылка стала None!

Слабые ссылки можно создавать на пользовательские классы, на set и на подклассы от dict и list, но не на сами dict и list. Встроенные типы tuple, int и подобные не поддерживают слабые ссылки (да и зачем они им?).  

В weakref.ref вторым аргументом можно передать функцию, которая будет вызвана при финализации объекта слабой ссылки:

weak_foo = weakref.ref(strong_foo, lambda r: print(f'finalizing {r}'))

weakref.getweakrefcount(object) и weakref.getweakrefs(object) позволяют получить количество слабых ссылок на объект и их сами.

weakref.proxy(object[, callback]) – создает слабый прокси-объект к объекту. Т.е. с ним можно обращаться также как и исходным, пока он не удалится. Попытка использовать прокси к уничтоженному объекту вызовет ReferenceError.

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

weakref.WeakSet – как set, но элементы хранятся по слабым ссылкам и удаляются, если на них больше нет сильных ссылок

weakref.WeakKeyDictionary – как dict, но КЛЮЧИ по слабым ссылкам.

weakref.WeakValueDictionary – как dict, но ЗНАЧЕНИЯ по слабым ссылкам.

📎 Пример:

import weakref

class Foo: ...
f1, f2 = Foo(), Foo()

weak_dict = weakref.WeakValueDictionary()
weak_dict["f1"] = f1
weak_dict["f2"] = f2

def print_weak_dict(wd):
    print('weak_dict: ', *wd.items())

print_weak_dict(weak_dict)  # оба в словаре

del f2
print_weak_dict(weak_dict)  # один ушел

del f1
print_weak_dict(weak_dict)  # ничего не осталось

📎 Наконец, можно следить за тем, когда объект будет удален:

f = Foo()
# просто установим обработчик (finalize сам никого не удаляет)
weakref.finalize(f, print, "object dead or program exit")
del f  # а вот тут print вызовется

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

♻️ Управление памятью и сборка мусора в Python.

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

Здесь я изложу основные тезисы об управлении памятью в Python (CPython). 

• В Python память управляется автоматически.

• Память для объектов, которые уже не нужны освобождается сборщиком мусора.

• Для небольших объектов (< 512 байт) Python выделяет и освобождает память блоками (в блоке может быть несколько объектов). Почему: операции с блоками памятью через ОС довольно долгие, а мелких объектов обычно много, и, таким образом, системные вызовы совершаются не так часто.

• Есть два алгоритма сборки мусора: подсчет ссылок (reference counting) и сборщик на основе поколений (generational garbage collector — gc).

• Алгоритм подсчета ссылок очень простой и эффективный, но у него есть один большой недостаток (помимо многих мелких). Он не умеет определять циклические ссылки

• Циклическими ссылками занимается gc, о ним чуть позже.

• Переменные хранят ссылки на объекты в памяти, внутри объект хранит числовое поле – количество ссылок на него (несколько переменных могут ссылаться на один объект)

• Количество ссылок увеличивается при присвоении, передаче аргументов в функцию, вставке объекта в список и т.п.

• Если число ссылок достигло 0, то объект сразу удаляется (это плюс).

• Если при удалении объект содержал ссылки на другие объекты, то и те могут удалиться, если это были последние ссылки.

• Переменные, объявленные вне функций, классов, блоков – глобальные.

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

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

• Функция sys.getrefcount позволит узнать число ссылок на объект (правда она накинет единицу, т.к. ее аргумент — тоже ссылка на тестируемый объект):

>>> foo = []
>>> import sys
>>> sys.getrefcount(foo)
2
>>> def bar(a): print(sys.getrefcount(a))
...
>>> bar(foo)
4
>>> sys.getrefcount(foo)
2

• Подсчет ссылок в CPython — исторически. Вокруг него много дебатов. В частности наличие GIL многим обязано этому алгоритму. 

• Пример создания циклической ссылки – добавим список в себя:

lst = []
lst.append(lst) 

• Цикличные ссылки обычно возникают в задачах на графы или структуры данных с отношениями между собой.

• Цикличные ссылки могут происходить только в “контейнерных” объектах (списки, словари, …).

• GC запускается переодически по особым условиям; запуск GC создает микропаузы в работе кода.

• GC разделяет все объекты на 3 поколения. Новые объекты попадают в первое поколение. 

• Как правило, большинство объектов живет недолго (пример: локальные переменные в функции). Поэтому сборка мусора в первом поколении выполняется чаще.

• Если новый объект выживает процесс сборки мусора, то он перемещается в следующее поколение. Чем выше поколение, тем реже оно сканируется на мусор. 

• Во время сборки мусора объекты поколения, где он собирается, сканируются на наличие циклических ссылок; если никаких ссылок, кроме циклических нет — то объекты удаляются.

• Можно использовать инструменты из модуля weakref для создания слабых ссылок. 

• Слабые ссылки не учитываются при подсчете ссылок. Если объект, на который ссылается слабая ссылка, удалится, то слабая ссылка просто обнулится, станет пустышкой.

• Подсчет ссылок не может быть отключен, а gc — может.

• В некоторых случаях полезно отключить автоматическую сборку gc.disable() и вызывать его вручную gc.collect().

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

🤭 В Python 3.8 будет оператор «морж»

Python 3.8 все еще в разработке, но уже можно полистать список грядущих изменений, и, пожалуй, самое значимое из них (и возможно единственное заметное изменение) – ввод нового оператора присваивания := (морж). Старички вспомнили Паскаль. Смысл этого оператора – дать имя результату выражения. Т.е. вычисляем правую часть моржа, связываем с именем переменной слева и возвращаем результат моржа наружу.

Раньше делали так:

x = len(s)
if x:
    print(x)

Будем делать так:

if x := len(s):  # можно в 3.8
    print(x)

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

📎 Пример. Используем вычисленное однажды значение f(x) под именем y:

[y := f(x), y**2, y**3]

📎 Пример. Читаем, сохраняем в chunk и сразу проверяем условие цикла:

while chunk := file.read(8192):
   process(chunk)

📎 Пример. Можно применить в проходах по спискам, чтобы дважды не вычислять f(x):

filtered_data = [y for x in data if (y := f(x)) is not None]

📎 Примеры можно/нельзя:

x := 5    # нельзя
(x := 5)  # можно
x = y := 0  # нельзя
x = (y := 0)  # можно

Приоритет запятой возле нового оператора. Сравните:

x = 1, 2     # x -> (1, 2)
(x := 1, 2)  # x -> 1

P.S.: Казалось бы, почему не сделать так: if x = len(s)? Ответ: чтобы не путать с if x == len(s). В C-подобных языках это частая проблема.

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