Я хочу иметь сопоставление dict с объектов активного фрейма (FrameType
) с некоторыми данными. Активный означает, что он находится в текущей трассировке стека выполнения.
Однако хранить ссылку на объект фрейма — плохая идея, потому что это также будет держать неактивные фреймы и очень скоро взорвет память из-за всех ссылок на все локальные переменные.
Естественным решением было бы использовать слабую ссылку на объекты фрейма, а точнее WeakKeyDictionary
.
Однако в настоящее время невозможно создать слабую ссылку на объект фрейма:
import weakref
import sys
f = sys._getframe()
weakref.ref(f)
урожаи
TypeError: cannot create weak reference to 'frame' object
Я предполагаю, что это деталь реализации CPython?
Итак, какие есть варианты?
Я мог бы в любом случае создать ссылки, но постараюсь очистить их как можно скорее, когда они больше не активны. Как на самом деле проверить, активен ли объект фрейма?
Вот сумасшедшая идея: если вы не можете хранить ссылку на фрейм, сделайте так, чтобы фрейм содержал ссылку на вас.
class FrameDict:
def __init__(self):
self._data_by_frame_id = {}
def __setitem__(self, frame, data):
frame_id = id(frame)
# Make the frame hold a reference to a KeyDeleter, so when the frame
# dies, the KeyDeleter dies, and the frame is removed from the dict
deleter = KeyDeleter(self._data_by_frame_id, frame_id)
frame.f_locals[deleter] = deleter
self._data_by_frame_id[frame_id] = data
def __getitem__(self, frame):
frame_id = id(frame)
return self._data_by_frame_id[frame_id]
class KeyDeleter:
def __init__(self, mapping, key):
self.mapping = mapping
self.key = key
def __del__(self):
del self.mapping[self.key]
Демо:
import inspect
def new_frame():
return inspect.currentframe()
frame_dict = FrameDict()
frame = new_frame()
frame_dict[frame] = 'foobar'
print(frame_dict[frame]) # foobar
del frame
print(frame_dict._data_by_frame_id) # {}
@Albert f_locals
— задокументированный атрибут; он указан в этот массивный стол. Я также не вижу проблемы с отображением на основе идентификатора, цепочка событий, которая должна вызвать ошибку, будет следующей: 1) кадр удаляется 2) новый кадр создается с идентификатором предыдущего кадра 3 ) вы ищете новый кадр в своем словаре 4) KeyDeleter.__del__
выполняется. Я не думаю, что есть реализация Python, в которой это может произойти.
Ах, вы спросили, безопасно ли модифицировать f_locals
. На самом деле это хороший вопрос. Я не уверен, но это работает ¯\_(ツ)_/¯
Это может привести к сбою внутри функции — в глобальной области видимости locals
и globals
одинаковы, но в области действия функции locals
представляет собой динамически построенное представление двух массивов фиксированного размера.
@MisterMiyagi Я использую локальные функции в своей демонстрации, все работает нормально. По крайней мере, в CPython.
Цепочка «1) удаленный кадр 2) новый кадр с идентификатором предыдущего кадра 3) __getitem__
4) KeyDeleter.__del__
» может произойти с любым Python на основе GC, особенно CPython. Может быть, не так часто. Но запустите это 1 миллион раз, и вы, вероятно, увидите это.
@Albert Хорошо, CPython также использует подсчет ссылок, а GC очищает только объекты, у которых есть цикл ссылок. Таким образом, вам сначала нужно создать эталонный цикл, содержащий кадр стека. И тогда вам каким-то образом придется вызывать FrameDict.__getitem__
, пока сборщик мусора занимается очисткой этого рефцикла. Но если вы все еще беспокоитесь, вы можете уменьшить вероятность столкновения идентификаторов, сохранив дополнительные идентификаторы. Например, вместо использования id(frame)
в качестве клавиши диктофона используйте (id(frame), id(frame.f_back), id(frame.f_locals))
.
Интересная идея. Но безопасно ли писать
frame.f_locals
? Другая проблема заключается в том, чтоid
уникален только во время жизни объектов, поэтому ваш__getitem__
может возвращать неверные данные.