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

В продолжение вчерашней темы, покажу, как можно визуализировать граф ссылок объектов в 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 collectorgc).

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

• Циклическими ссылками занимается 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?

1) Нам понадобится Selenium, чтобы управлять браузером. Документация по Selenium

pip install selenium

2) Для примера будем управлять популярным браузером Chrome. Для него отдельно придется скачать ChromeDriver

Установка на MacOS и Linux происходит через команды в терминале, чтобы исполняемый файл драйвера был доступен в окружении (PATH):

mv chromedriver /usr/local/bin/
chmod +x /usr/local/bin/chromedriver

3) Переходим к коду на Python. Создадим наш веб-драйвер:

from selenium import webdriver
DRIVER = 'chromedriver'
driver = webdriver.Chrome(DRIVER)

Отправляем запрос к интересующей нас веб-странице:

driver.get('https://erugame.ru/') 

Делаем скриншот и сохраняем его под нужным именем:

driver.save_screenshot("screenshot.png")

Завершаем работу, закрывая окно браузера:

driver.quit()

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

element = driver.find_element_by_tag_name('body')
element.screenshot("screenshot_full.png")

Как видите, все просто! Полный код примера здесь.

Специально для канала @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. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Пары из списка

В Python есть элегантный прием, который позволяет получить пары соседних элементов из списка. Нужно использовать функцию zip, передав в нее сам список и его же со сдвигом 1:

a = [1, 2, 3, 4, 5, 6]

for x1, x2 in zip(a, a[1:]):
    print(x1, x2)

Вывод:

1 2
2 3
3 4
4 5
5 6

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