На этом шаге мы поговорим о слабых ссылках.
Ваша программа создает структуры данных с циклами (например, деревья, графы, паттерны проектирования типа "Наблюдатель" и т. п.), и у вас проблемы с управлением памятью.
Простой пример циклической структуры данных - это дерево, в котором родитель указывает на потомков, а потомки указывают на родителя. При работе с такой структурой вы должны задуматься над тем, чтобы сделать слабым один из типов ссылок, применив библиотеку weakref. Например:
>>> import weakref >>> 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') >>> c1 = Node('child') >>> root.add_child(c1) >>> c1.parent Node('parent') >>> del root >>> c1.parent Node('parent') >>>
Циклические структуры данных - это сложный аспект Python, который требует тщательного изучения, поскольку обычные правила сборки мусора к ним часто не применимы. Например, рассмотрите такой код:
# Класс, созданный, чтобы проиллюстрировать то, что будет при удалении class Data: def __del__(self): print('Data.__del__') # Класс-узел, в котором есть цикл class Node: def __init__(self): self.data = Data() self.parent = None self.children = [] def add_child(self, child): self.children.append(child) child.parent = self
Теперь поэкспериментируем с этим кодом, чтобы обнаружить некоторые тонкости сборки мусора:
>>> a = Data() >>> del a # Сразу же удаляется Data.__del__ >>> a = Node() >>> del a # Сразу же удаляется Data.__del__ >>> a = Node() >>> a.add_child(Node()) >>> del a # Не удаляется (и сообщение не выводится) >>>
Как вы можете видеть, объекты немедленно удаляются - за исключением последнего случая, когда в объекте присутствует цикл. Причина в том, что сборщик мусора Python построен на принципе простого подсчета ссылок. Когда счетчик ссылок объекта достигает 0, объект сразу же удаляется. В случае циклических структур данных это никогда не произойдет. В последней части примера узел-родитель и узел-потомок ссылаются друг на друга, что поддерживает счетчик ссылок ненулевым.
Чтобы разобраться с циклами, периодически запускается отдельный специальный сборщик мусора. Однако в качестве общего правила стоит учитывать, что вы не знаете, когда именно он запустится. Следовательно, вы не знаете, когда циклические структуры данных будут удалены. При необходимости вы можете принудительно запустить сборку мусора, но это не лучший выход:
>>> import gc >>> gc.collect() # Принудительный запуск Data.__del__ Data.__del__ >>>
Еще более неприятная проблема возникает, если объекты, вовлеченные в цикл, определяют собственный метод __del__(). Предположим, что у нас есть вот такой код:
# Класс, созданный, чтобы проиллюстрировать то, что будет при удалении class Data: def __del__(self): print('Data.__del__') # Класс-узел, использующий цикл class Node: def __init__(self): self.data = Data() self.parent = None self.children = [] # НИКОГДА ТАК НЕ ДЕЛАЙТЕ. # ПОКАЗАНО ТОЛЬКО ДЛЯ ДЕМОНСТРАЦИИ ПАТОЛОГИЧЕСКОГО ПОВЕДЕНИЯ def __del__(self): del self.data del self.parent del self.children def add_child(self, child): self.children.append(child) child.parent = self
В этом случае структура данных никогда не будет удалена сборщиком мусора, и ваша программа вызовет утечку памяти! Если вы попробуете применять этот код, то увидите, что сообщение Data.__del__ никогда не появляется - даже при принудительном запуске сборки мусора:
>>> a = Node() >>> a.add_child(Node()) >>> del a # Нет сообщения (не собрано) >>> import gc >>> gc.collect() # Нет сообщения (не собрано)
Слабые ссылки решают эту проблему путем устранения ссылочных циклов. Слабая ссылка - это указатель на объект, который не увеличивает его счетчик ссылок. Создавать слабые ссылки можно с помощью библиотеки weakref:
>>> import weakref >>> a = Node() >>> a_ref = weakref.ref(a) >>> a_ref <weakref at 0x100581f70; to 'Node' at 0x1005c5410> >>>
Чтобы разрешить (dereference) слабую ссылку, вы должны вызвать ее как функцию. Если объект все еще существует, он будет возвращен. В противном случае будет возвращено значение None. Поскольку счетчик ссылок изначального объекта не увеличивался, он может быть удален обычным способом. Например:
>>> print(a_ref()) <__main__.Node object at 0x1005c5410> >>> del a Data.__del__ >>> print(a_ref()) None >>>
Если вы будете использовать слабые ссылки, как показано в решении, то вы обнаружите, что ссылочные циклы не возникают, и сборка мусора происходит сразу же после того, как узел перестает использоваться.
На следующем шаге мы заставим классы поддерживать операции сравнения.