🧙‍♂️ Магия Jupyter Notebook

Jupyter Notebook предлагает богатейшие возможности по прототипированию кода, проверке гипотез, демонстраций и научных трудов в сравнении со стандартным интерпретатором Python.

Уставновка:

pip install jupyter

Запуск. В терминале пишем:

jupyter notebook

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

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

Рассмотрим некоторые из них:

%magic — выведет документацию по всем-всем доступным магическим функциям.

%lsmagic — просто список этих функций.

%timeit – измеряет среднее время выполнения кусочка кода, при этом вывод гораздо более информативен, чем обычный вызов timeit.timeit; и не требует лишних import.

Сравните вот это (из Jupyter):

def test():
  return sum(range(1000))
%timeit test()

12.5 µs ± 378 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

С этим (из интерпретатора):

>>> import timeit
>>> def test(): return sum(range(1000))
...
>>> timeit.timeit("test()", "from __main__ import test")
12.405041060002986

По-моему, первый вариант выигрывает по удобству и информативности.

%%timeit – многострочный вариант предыдущей функции. Пример:

%%timeit x = 10
x += 20
x /= 2
48.7 ns ± 0.435 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%pinfo [имя] или [имя]? – покажет документацию по функции или классу [имя]. Примеры:

import numpy as np
%pinfo np.random.uniform

Или

import numpy as np
np.random.uniform?

Построение графиков:

%matplotlib inline
from matplotlib import pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.plot(x, y)

%env — показать текущие переменные среды.

%env [имя]=[значение] — управление переменными среды. Пример:

%env OMP_NUM_THREADS=4

%cd – показывает или меняет рабочую директорию.

Можно вызывать системные команды прямо из блокнота через знак восклицания. Примеры:

!ls
!pip install click
# резульат выполнения системной команды можно получить в перемунную и использовать далее
output = !pip list | grep tensorflow

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

P.S. Многие магические функции также работают и в интерпретаторе IPython.

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

📕 Удаление ключа из словаря

Словарь (dict) – изменяемый тип в Python. Из словаря можно легко удалить ключ оператором del:

>>> d = {"foo":123, "bar":321}
>>> del d["foo"]
>>> d
{'bar': 321}

Что если ключа не окажется в словаре? Ответ: исключение – KeyError:

>>> del d['baz']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'baz'

Конечно, можно сделать так:

if 'baz' in d:
    del d['baz']

Или даже так:

try:
    del d['baz']
except KeyError:
    pass

Однако, есть способ удалить ключ (которого возможно нет) в одну строчку:

d.pop('baz', None)

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

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

📕 dict() или {}

Разработчики предпочитают разные способы создания пустого словаря. Но равнозначны ли они?

Оказывается, что нет. Они приводят к генерации разного байт-кода. Убедимся в этом с помощью модуля dis:

 >>> import dis
 >>> dis.dis('{}')
   1           0 BUILD_MAP                0
               2 RETURN_VALUE
 >>> dis.dis('dict()')
   1           0 LOAD_NAME                0 (dict)
               2 CALL_FUNCTION            0
               4 RETURN_VALUE 

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

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

>>> from timeit import timeit
>>> timeit('{}')
0.03544308300479315
>>> timeit('dict()')
0.08697152900276706

Вывод: dict() – работает значительно медленнее. Не призываем переписывать старый код, просто на заметку.

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

​​🔄 Визуализация графа ссылок

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

0) Для рисования графов понадобится graphviz Например, на MacOS вы можете установить его через Homebrew:

brew install graphviz

1) Установим библиотеку objgraph:

pip install objgraph

2) Использование. Пусть у нас есть такая структура данных:

x = ["test"]
x.append(x)
y = [x, [x], dict(x=x), set([1, 2, "test"])]

Сохраняем граф ссылок на объекты, на которые ссылается y в файл ‘1.png’. Обратите внимание, что show_refs принимает именно список [y], а не просто y:

import objgraph
objgraph.show_refs([y], filename='1.png')

Можно для каждого объекта вывести общее число ссылок на него:

objgraph.show_refs([y], refcounts=True, filename='2.png')
Пример вывода objgraph

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

objgraph.show_backrefs([x], filename='3-back.png')

Узнать статистику по самым распространенным объектам в текущей среде:

>>> objgraph.show_most_common_types(limit=5)
function           2127
dict               1193
wrapper_descriptor 1002
tuple              954
weakref            868

Или по конкретному типу глобально:

>>> objgraph.count('dict')
1195

Или среди конкретного списка объектов:

>>> objgraph.count('dict', [{'x':5}, {'y':6}])
2

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

👉 Общая документация по objgraph 

👉 Список функций objgraph

👨‍🎓Специально для канала @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.