[Перевод] Руководство по слабым ссылкам в Python с применением модуля weakref

1fd7b437dd423730081e0a08617765eb
Вполне вероятно, что вы никогда не сталкивались с модулем 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. Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.

© Habrahabr.ru