Как подключиться к сигналу GObject в python, не сохраняя ссылку на коннектор?

Проблема в основном в этом, в привязках python gobject и gtk. Предположим, у нас есть класс, который привязывается к сигналу при создании:

class ClipboardMonitor (object):
  def __init__(self):
    clip = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
    clip.connect("owner-change", self._clipboard_changed)

Проблема в том, что ни один экземпляр ClipboardMonitor никогда не умрет. Буфер обмена gtk является объектом всего приложения, и при подключении к нему сохраняется ссылка на объект, поскольку мы используем обратный вызов self._clipboard_changed.

Я обсуждаю, как обойти это с помощью слабых ссылок (модуль weakref), но мне еще предстоит придумать план. У кого-нибудь есть идея, как передать обратный вызов в регистрацию сигнала и заставить его вести себя как слабая ссылка (если обратный вызов сигнала вызывается, когда экземпляр ClipboardMonitor находится вне области действия, он должен быть неоперативным).

Дополнение: Фразировано независимо от GObject или GTK+:

Как вы предоставляете метод обратного вызова непрозрачному объекту с семантикой слабой ссылки? Если подключаемый объект выходит за пределы области видимости, его следует удалить, а обратный вызов должен действовать как отсутствие операции; подключаемый объект не должен содержать ссылку на коннектор.

Чтобы уточнить: я явно хочу избежать вызова метода «деструктор/финализатор».


person u0b34a0f6ae    schedule 01.09.2009    source источник
comment
Дополнительная идея: если вам не нужно отключение, немного хакерская идея: clip.connect(foo, (lambda *things,obj=weakref.ref(self):(obj().method(*things) if obj() else None))) -- не очень красиво, но острота!   -  person liori    schedule 02.09.2009


Ответы (3)


Стандартный способ - отключить сигнал. Однако для этого в вашем классе должен быть метод, подобный деструктору, который явно вызывается кодом, который поддерживает ваш объект. Это необходимо, потому что иначе вы получите циклическую зависимость.

class ClipboardMonitor(object):
    [...]

    def __init__(self):
        self.clip = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
        self.signal_id = self.clip.connect("owner-change", self._clipboard_changed)

    def close(self):
        self.clip.disconnect(self.signal_id)

Как вы указали, вам нужны слабые ссылки, если вы хотите избежать явного уничтожения. Я бы написал слабую фабрику обратного вызова, например:

import weakref

class CallbackWrapper(object):
    def __init__(self, sender, callback):
        self.weak_obj = weakref.ref(callback.im_self)
        self.weak_fun = weakref.ref(callback.im_func)
        self.sender = sender
        self.handle = None

    def __call__(self, *things):
        obj = self.weak_obj()
        fun = self.weak_fun()
        if obj is not None and fun is not None:
            return fun(obj, *things)
        elif self.handle is not None:
            self.sender.disconnect(self.handle)
            self.handle = None
            self.sender = None

def weak_connect(sender, signal, callback):
    wrapper = CallbackWrapper(sender, callback)
    wrapper.handle = sender.connect(signal, wrapper)
    return wrapper

(это доказательство концептуального кода, работает для меня - вам, вероятно, следует адаптировать этот фрагмент к вашим потребностям). Несколько заметок:

  • Я сохраняю объект обратного вызова и функцию отдельно. Вы не можете просто сделать слабую ссылку связанного метода, потому что связанные методы являются очень временными объектами. На самом деле weakref.ref(obj.method) уничтожит связанный объект метода сразу после создания слабой ссылки. Я не проверял, нужно ли также хранить слабую ссылку на функцию... Думаю, если ваш код статичен, вы, вероятно, можете этого избежать.
  • Оболочка объекта удалит себя из отправителя сигнала, когда заметит, что слабая ссылка перестала существовать. Это также необходимо для разрушения циклической зависимости между CallbackWrapper и объектом-отправителем сигнала.
person liori    schedule 01.09.2009
comment
Хороший! Я опубликовал ответ, чтобы показать, что я придумал, но вы предоставили некоторые недостающие части. Хорошо Вопросы: Атрибуты im_self, im_func задокументированы/надежны? Я использую имя объекта/атрибута. А отключение сигнала — это бонус, а не строгое требование. - person u0b34a0f6ae; 02.09.2009
comment
Они определены в docs.python.org/reference/ (ищите Пользовательские методы). - person liori; 02.09.2009

(Этот ответ отслеживает мой прогресс)

Эта вторая версия также отключится; У меня есть удобная функция для gobjects, но на самом деле мне нужен этот класс для более общего случая — как для обратных вызовов сигнала D-Bus, так и для обратных вызовов GObject.

В любом случае, как можно назвать стиль реализации WeakCallback? Это очень чистая инкапсуляция слабого обратного вызова, но с незаметно добавленной специализацией gobject/dbus. Лучше написать два подкласса для этих двух случаев.

import weakref

class WeakCallback (object):
    """A Weak Callback object that will keep a reference to
    the connecting object with weakref semantics.

    This allows to connect to gobject signals without it keeping
    the connecting object alive forever.

    Will use @gobject_token or @dbus_token if set as follows:
        sender.disconnect(gobject_token)
        dbus_token.remove()
    """
    def __init__(self, obj, attr):
        """Create a new Weak Callback calling the method @obj.@attr"""
        self.wref = weakref.ref(obj)
        self.callback_attr = attr
        self.gobject_token = None
        self.dbus_token = None

    def __call__(self, *args, **kwargs):
        obj = self.wref()
        if obj:
            attr = getattr(obj, self.callback_attr)
            attr(*args, **kwargs)
        elif self.gobject_token:
            sender = args[0]
            sender.disconnect(self.gobject_token)
            self.gobject_token = None
        elif self.dbus_token:
            self.dbus_token.remove()
            self.dbus_token = None

def gobject_connect_weakly(sender, signal, connector, attr, *user_args):
    """Connect weakly to GObject @sender's @signal,
    with a callback in @connector named @attr.
    """
    wc = WeakCallback(connector, attr)
    wc.gobject_token = sender.connect(signal, wc, *user_args)
person u0b34a0f6ae    schedule 01.09.2009

на самом деле еще не пробовал, но:

class WeakCallback(object):
    """
    Used to wrap bound methods without keeping a ref to the underlying object.
    You can also pass in user_data and user_kwargs in the same way as with
    rpartial. Note that refs will be kept to everything you pass in other than
    the callback, which will have a weakref kept to it.
    """
    def __init__(self, callback, *user_data, **user_kwargs):
        self.im_self = weakref.proxy(callback.im_self, self._invalidated)
        self.im_func = weakref.proxy(callback.im_func)
        self.user_data = user_data
        self.user_kwargs = user_kwargs

    def __call__(self, *args, **kwargs):
        kwargs.update(self.user_kwargs)
        args += self.user_data
        self.im_func(self.im_self, *args, **kwargs)

    def _invalidated(self, im_self):
        """Called by the weakref.proxy object."""
        cb = getattr(self, 'cancel_callback', None)
        if cb is not None:
            cb()

    def add_cancel_function(self, cancel_callback):
        """
        A ref will be kept to cancel_callback. It will be called back without
        any args when the underlying object dies.
        You can wrap it in WeakCallback if you want, but that's a bit too
        self-referrential for me to do by default. Also, that would stop you
        being able to use a lambda as the cancel_callback.
        """
        self.cancel_callback = cancel_callback

def weak_connect(sender, signal, callback):
    """
    API-compatible with the function described in
    http://stackoverflow.com/questions/1364923/. Mostly used as an example.
    """
    cb = WeakCallback(callback)
    handle = sender.connect(signal, cb)
    cb.add_cancel_function(WeakCallback(sender.disconnect, handle))
person Community    schedule 12.11.2009