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

Недавно в заметке про управление памятью в 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. Возможно, кому-то это поможет решить сложные моменты с использованием памяти и с организацией нетривиальных структур данных.

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! 👈