[Перевод] Руководство по слабым ссылкам в Python с применением модуля weakref
weakref
языка Python и, возможно, даже не слышали о нём. Притом, что ваш код может быть написан и почти без применения слабых ссылок, этот модуль фундаментально важен для внутреннего устройства многих библиотек, фреймворков и самого языка Python. Так что в этой статье мы исследуем, что он собой представляет, чем может быть полезен, и каким образом этот модуль вам было бы удобно встраивать в ваш собственный код.Основы
Чтобы понять модуль
weakref
и слабые ссылки, давайте сначала немного подробнее выясним, как в Python происходит сборка мусора.В качестве механизма, регулирующего сборку мусора, Python использует подсчёт ссылок. Проще говоря, Python ведёт счёт ссылок для каждого создаваемого нами объекта, и счёт ссылок увеличивается на единицу всякий раз, когда на объект ставится очередная ссылка в коде. Когда ссылка с объекта снимается (например, переменная устанавливается в None). Если в какой-то момент количество ссылок падает до нуля, это означает, что вся память, выделенная под объект, у него изымается, и в таком случае объект попадает под сборку мусора.
Чтобы было понятнее, давайте разберём пример кода:
import sys
class SomeObject:
def __del__(self):
print(f"(Deleting {self=})")
obj = SomeObject()
print(sys.getrefcount(obj)) # 2
obj2 = obj
print(sys.getrefcount(obj)) # 3
obj = None
obj2 = None
# (Deleting self=<__main__.SomeObject object at 0x7d303fee7e80>)
Здесь мы определяем класс, реализующий лишь метод
__del__
. Этот метод вызывается, когда метод подпадает под сборку мусора. Мы пишем такой код, именно чтобы сразу было видно, когда произойдёт сборка мусора.Создав экземпляр этого класса, можем узнать актуальное количество ссылок, указывающих на этот объект, при помощи метода sys.getrefcount
. Логично ожидать, что здесь мы получим 1, но обычно getrefcount возвращает число на 1 больше, чем можно было бы рассчитывать. Дело в том, что при вызове getrefcount
ссылка копируется по значению в аргумент функции, из-за чего количество ссылок на данный объект временно подрастает.
Далее, если объявить obj2 = obj
и снова вызвать getrefcount, мы получим 3, так как теперь ссылки на экземпляр одновременно направлены с obj
и obj2
. Напротив, если присвоить этим переменным None, то количество ссылок снизится до нуля, и в конце концов мы получим от метода __del__
сообщение о том, что объект попал под сборку мусора.
Как же во всю эту картину вписываются слабые ссылки? Если все оставшиеся ссылки на объект являются слабыми, то, с точки зрения интерпретатора Python, этот объект подпадает под сборку мусора. Иными словами, слабая ссылка — недостаточный довод в пользу того, чтобы оставлять данный объект в живых:
import weakref
obj = SomeObject()
reference = weakref.ref(obj)
print(reference) #
print(reference()) # <__main__.SomeObject object at 0x707038c0b700>
print(obj.__weakref__) #
print(sys.getrefcount(obj)) # 2
obj = None
# (Deleting self=<__main__.SomeObject object at 0x70744d42b700>)
print(reference) #
print(reference()) # None
Здесь мы опять объявляем в нашем классе переменную
obj
, но на сей раз создаём на этот объект не вторую сильную ссылку, а слабую ссылку, которую помещаем в переменную reference
.Если затем проверить, сколько у нас ссылок, окажется, что счёт ссылок не возрос. Если же установить переменную obj
в None
, то увидим, что она сразу же подпадает под сборку мусора, несмотря на то, что слабая ссылка на неё до сих пор существует.
Наконец, если попытаться обратиться по слабой ссылке к тому объекту, который уже попал под сборку мусора, мы получим «мёртвую» ссылку и, соответственно, None
.
Также обратите внимание: если мы попытаемся обратиться к объекту по слабой ссылке, то всё равно будем должны вызвать его как функцию (reference())
— только так можно извлечь объект. Поэтому чаще бывает удобнее воспользоваться прокси (посредником), в особенности, если требуется доступ к атрибутам объекта:
obj = SomeObject()
reference = weakref.proxy(obj)
print(reference) # <__main__.SomeObject object at 0x78a420e6b700>
obj.attr = 1
print(reference.attr) # 1
Когда этим можно пользоваться
Теперь, зная, как работают слабые ссылки, давайте на нескольких примерах рассмотрим, в каких случаях они могут быть полезны.
Типичный контекст для работы со слабыми ссылками — древовидные структуры данных:
class Node:
def __init__(self, value):
self.value = value
self._parent = None
self.children = []
def __repr__(self):
return "Node({!r:})".format(self.value)
@property
def parent(self):
return self._parent if self._parent is None else self._parent()
@parent.setter
def parent(self, node):
self._parent = weakref.ref(node)
def add_child(self, child):
self.children.append(child)
child.parent = self
root = Node("parent")
n = Node("child")
root.add_child(n)
print(n.parent) # Node('parent')
del root
print(n.parent) # None
Здесь мы реализуем дерево при помощи класса
Node
, где с дочерних узлов на родительский направлены слабые ссылки. В таком смысле дочерний Node
может существовать и без родительского Node
, что позволяет бесшумно удалить родителя/собрать как мусор. Если нужно, то эту ситуацию можно превратить и в обратную:
class Node:
def __init__(self, value):
self.value = value
self._children = weakref.WeakValueDictionary()
@property
def children(self):
return list(self._children.items())
def add_child(self, key, child):
self._children[key] = child
root = Node("parent")
n1 = Node("child one")
n2 = Node("child two")
root.add_child("n1", n1)
root.add_child("n2", n2)
print(root.children) # [('n1', Node('child one')), ('n2', Node('child two'))]
del n1
print(root.children) # [('n2', Node('child two'))]
Здесь родитель ведёт словарь слабых ссылок на своих потомков. В данном случае используется
WeakValueDictionary
— всякий раз, когда повсюду в программе убираются все до одной (слабые) ссылка на элемент, присутствующий в этом словаре, он автоматически удаляется и из самого словаря. Таким образом, мы можем отдельно не заниматься жизненным циклом элементов, присутствующих в этом словаре.Кроме того, weakref
можно использовать в рамках паттерна проектирования Наблюдатель:
class Observable:
def __init__(self):
self._observers = weakref.WeakSet()
def register_observer(self, obs):
self._observers.add(obs)
def notify_observers(self, *args, **kwargs):
for obs in self._observers:
obs.notify(self, *args, **kwargs)
class Observer:
def __init__(self, observable):
observable.register_observer(self)
def notify(self, observable, *args, **kwargs):
print("Got", args, kwargs, "From", observable)
subject = Observable()
observer = Observer(subject)
subject.notify_observers("test", kw="python")
# Got ('test',) {'kw': 'python'} From <__main__.Observable object at 0x757957b892d0>
В классе
Observable
хранятся слабые ссылки на наблюдающие объекты, поскольку его работа не зависит от того, будут ли они удалены. Как и в предыдущих примерах, так мы избавляемся от необходимости управлять жизненным циклом зависящих от него объектов. Вероятно, вы заметили, что в этом примере мы воспользовались WeakSet
— это ещё один класс из модуля weakref, функционально аналогичный WeakValueDictionary
, но реализуемый при помощи Set
.Последний пример из этого раздела взят из документации по weakref:
import tempfile, shutil
from pathlib import Path
class TempDir:
def __init__(self):
self.name = tempfile.mkdtemp()
self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)
def __repr__(self):
return "TempDir({!r:})".format(self.name)
def remove(self):
self._finalizer()
@property
def removed(self):
return not self._finalizer.alive
tmp = TempDir()
print(tmp) # TempDir('/tmp/tmp8o0aecl3')
print(tmp.removed) # False
print(Path(tmp.name).is_dir()) # True
Здесь во всей красе видим ещё одну возможность модуля
weakref
, а именно работу с weakref.finalize
. Как понятно из названия, он позволяет выполнить завершающую функцию/обратный вызов и в том случае, когда зависимый объект попал под сборку мусора. В такой ситуации реализуем класс TempDir
, с помощью которого можно создать временный каталог. В идеале нужно всегда держать в уме, что от TempDir
нужно избавляться сразу же, как только в нём исчезнет необходимость. Но, если мы об этом забудем, у нас будет под рукой такой завершитель, который автоматически применит rmtree
к каталогу, как только TempDir
попадёт под сборку мусора, в том числе, и при окончательном выходе из программы. Примеры из реальной практики
В предыдущем разделе было рассмотрено несколько примеров практического применения
weakref
, но давайте разберём и по-настоящему реалистичные примеры. Таков, например, случай, в котором создаётся кэшированный экземпляр: import logging
a = logging.getLogger("first")
b = logging.getLogger("second")
print(a is b) # False
c = logging.getLogger("first")
print(a is c) # True
Выше в простейшем виде используется встроенный модуль Python
logging
. Как видим, он позволяет ассоциировать с конкретным именем всего один экземпляр логгера. Таким образом, если мы попытаемся многократно извлекать один и тот же логгер, возвращаться нам будет один и тот же экземпляр логгера — сохранённый в кэше.Если бы мы хотели реализовать такую возможность, то код выглядел бы примерно так:
class Logger:
def __init__(self, name):
self.name = name
_logger_cache = weakref.WeakValueDictionary()
def get_logger(name):
if name not in _logger_cache:
l = Logger(name)
_logger_cache[name] = l
else:
l = _logger_cache[name]
return l
a = get_logger("first")
b = get_logger("second")
print(a is b) # False
c = get_logger("first")
print(a is c) # True
Наконец, сам Python использует слабые ссылки, например, при реализации
OrderedDict
: from _weakref import proxy as _proxy
class OrderedDict(dict):
def __new__(cls, /, *args, **kwds):
self = dict.__new__(cls)
self.__hardroot = _Link()
self.__root = root = _proxy(self.__hardroot)
root.prev = root.next = root
self.__map = {}
return self
Выше приведён отрывок
из модуля collections
языка CPython. Здесь weakref.proxy
используется для недопущения циклических ссылок (подробнее о них — в текстах docstring).Заключение
Возможность
weakref
— далеко не самая понятная, но временами без такого инструмента в арсенале просто не обойтись. Слабые ссылки могут очень пригодиться при реализации кэшей или таких структур данных, в которых присутствуют циклические ссылки — например, двойных связных списков. При этом нужно уметь поддерживать weakref
. Всё, сказанное здесь и в документации специфично для CPython, но в других реализациях Python weakref
также будут вести себя иначе. Кроме того, многие встроенные типы Python — например, list
, tuple
или int
— не поддерживают работу со слабыми ссылками.
P.S. Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.