classification
Title: In place operators of weakref.proxy() not returning self.
Type: behavior Stage:
Components: Library (Lib) Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: grahamd, ncoghlan, shishkander, tim.peters
Priority: normal Keywords:

Created on 2013-09-22 10:35 by grahamd, last changed 2020-11-15 19:31 by iritkatriel.

Files
File name Uploaded Description Edit
t.py shishkander, 2013-10-07 06:10 Test to reproduce
Messages (8)
msg198257 - (view) Author: Graham Dumpleton (grahamd) Date: 2013-09-22 10:35
When a weakref.proxy() is used to wrap a class instance which implements in place operators, when one applies the in place operator to the proxy, one could argue the variable holding the proxy should still be a reference to the proxy after the in place operator has been done. Instead the variable is replaced with the class instance the proxy was wrapping.

So for the code:

from __future__ import print_function

import weakref

class Class(object):
     def __init__(self, value):
         self.value = value
     def __iadd__(self, value):
         self.value += value
         return self

c = Class(1)

p = weakref.proxy(c)

print('p.value', p.value)
print('type(p)', type(p))

p += 1

print('p.value', p.value)
print('type(p)', type(p))

one gets:

$ python3.3 weakproxytest.py
p.value 1
type(p) <class 'weakproxy'>
p.value 2
type(p) <class '__main__.Class'>

One might expect type(p) at the end to still be <class 'weakproxy'>.

In the weakref.proxy() C code, all the operators are set up with preprocessor macros.

#define WRAP_BINARY(method, generic) \
    static PyObject * \
    method(PyObject *x, PyObject *y) { \
        UNWRAP(x); \
        UNWRAP(y); \
        return generic(x, y); \
    }

#define WRAP_TERNARY(method, generic) \
    static PyObject * \
    method(PyObject *proxy, PyObject *v, PyObject *w) { \
        UNWRAP(proxy); \
        UNWRAP(v); \
        if (w != NULL) \
            UNWRAP(w); \
        return generic(proxy, v, w); \
    }

These are fine for:

WRAP_BINARY(proxy_add, PyNumber_Add)
WRAP_BINARY(proxy_sub, PyNumber_Subtract)
WRAP_BINARY(proxy_mul, PyNumber_Multiply)
WRAP_BINARY(proxy_div, PyNumber_Divide)
WRAP_BINARY(proxy_floor_div, PyNumber_FloorDivide)
WRAP_BINARY(proxy_true_div, PyNumber_TrueDivide)
WRAP_BINARY(proxy_mod, PyNumber_Remainder)
WRAP_BINARY(proxy_divmod, PyNumber_Divmod)
WRAP_TERNARY(proxy_pow, PyNumber_Power)
WRAP_BINARY(proxy_lshift, PyNumber_Lshift)
WRAP_BINARY(proxy_rshift, PyNumber_Rshift)
WRAP_BINARY(proxy_and, PyNumber_And)
WRAP_BINARY(proxy_xor, PyNumber_Xor)
WRAP_BINARY(proxy_or, PyNumber_Or)

Because a result is being returned and the original is not modified.

Use of those macros gives the unexpected result for:

WRAP_BINARY(proxy_iadd, PyNumber_InPlaceAdd)
WRAP_BINARY(proxy_isub, PyNumber_InPlaceSubtract)
WRAP_BINARY(proxy_imul, PyNumber_InPlaceMultiply)
WRAP_BINARY(proxy_idiv, PyNumber_InPlaceDivide)
WRAP_BINARY(proxy_ifloor_div, PyNumber_InPlaceFloorDivide)
WRAP_BINARY(proxy_itrue_div, PyNumber_InPlaceTrueDivide)
WRAP_BINARY(proxy_imod, PyNumber_InPlaceRemainder)
WRAP_TERNARY(proxy_ipow, PyNumber_InPlacePower)
WRAP_BINARY(proxy_ilshift, PyNumber_InPlaceLshift)
WRAP_BINARY(proxy_irshift, PyNumber_InPlaceRshift)
WRAP_BINARY(proxy_iand, PyNumber_InPlaceAnd)
WRAP_BINARY(proxy_ixor, PyNumber_InPlaceXor)
WRAP_BINARY(proxy_ior, PyNumber_InPlaceOr)

This is because the macro returns the result from the API call, such as PyNumber_InPlaceAdd() where as it should notionally be returning 'proxy' so that the variable holding the weakref proxy instance is set to the proxy object again and not the result of the inner API call.

In changing this though there is a complication which one would have to deal with.

If the result of the inner API call such as PyNumber_InPlaceAdd() is the same as the original object wrapped by the weakref proxy, then all is fine.

What though should be done if it is different as notionally this means that the reference to the wrapped object would need to be changed to the new value.

The issue is that if one had to replace the reference to the wrapped object with a different one due to the in place operator, then notionally the whole existence of that weakref is invalidated as you would no longer be tracking the same object the weakref proxy was created for.

This odd situation is perhaps why the code was originally written the way it was, although that then sees the weakref proxy being replaced which could cause different problems with the callback not later being called since the weakref proxy can be destroyed before the object it wrapped. As there is nothing in the documentation of the code which calls out such a decision, not sure if it was deliberate or simply an oversight.

Overall I am not completely sure what the answer should be, so I am logging it as interesting behaviour. Maybe this odd case needs to be called out in the documentation in some way at least. That or in place operators simply shouldn't be allowed on a weakref proxy because of the issues it can cause either way.
msg198262 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-09-22 11:23
Ouch :P

Perhaps the least-incompatible fix would be to switch to returning the proxy if the object returned is the proxied object (which would do the right thing for proxies to mutable objects), while preserving the current behaviour when the in-place operation returns a new object?

That behaviour is still somewhat broken, but there's no obviously non-broken behaviour in that case, and this broadly preserves compatibility with the status quo.
msg199130 - (view) Author: (shishkander) Date: 2013-10-07 06:10
This bug happens not just with operators, but with any call self.call_something()! For example, running this:

    from __future__ import print_function
    import weakref

    class Test(object):
        def method(self):
            return type(self)

    def test(obj):
        print(type(obj), "=>", obj.method())

    o = Test()
    test(o)
    test(weakref.proxy(o))


(also attached for convenience as t.py) 
produces in both python2.7 and python3.2:

$ python t.py 
<class '__main__.Test'> => <class '__main__.Test'>
<type 'weakproxy'> => <class '__main__.Test'>
msg199132 - (view) Author: Graham Dumpleton (grahamd) Date: 2013-10-07 08:22
@shishkander I can't see how what you are talking about has got anything to do with the issue with in place operators. The results from your test script are expected and normal.

What result are you expecting?

The one thing you cannot override in Python is what type() returns for an object. Thus is it completely normal for the weakref.proxy object to have a different type that what it wraps.

This is one of the reasons why in Python why should rarely ever do direct comparison of type objects. Instead you should use isinstance().

>>> import weakref
>>> class Test(object):
...     pass
...
>>> test = Test()
>>> proxy = weakref.proxy(test)
>>> type(test)
<class '__main__.Test'>
>>> type(proxy)
<type 'weakproxy'>
>>> isinstance(test, Test)
True
>>> isinstance(proxy, Test)
True
>>> proxy.__class__
<class '__main__.Test'>

The isinstance() check will work because weakref.proxy will proxy __class__() method such that it returns the type of the wrapped object rather than of the proxy.

Now if your problem is with methods of wrapped objects which return self not having that self object some how automatically wrapped in another proxy, there isn't anything the proxy can be do about that. That is a situation where you as a user need to be careful about what you are doing. A way one can handle that is through derivation off a proxy object and override specific methods where you then in turn need to wrap the result, but I can see that easily becoming fragile when weakrefs are involved. Also, the weakref proxy in Python doesn't expose a class for doing that anyway.

One important thing to note is that where self is returned is returned by a normal method, it is still on you to have assigned the result to a variable so as to have started any possible problems. In the case of in place operators that is done under the covers by Python and you have no control over it. This is why the current behaviour as originally described is arguably broken as is breaks the expectations of what would logically happen for an in place operator when used via a proxy, something you have no control over.

So can you go back and explain what your specific problem is that you believe is the same issue as this bug report is, because so far I can't see any similarity based on your example code.
msg199133 - (view) Author: (shishkander) Date: 2013-10-07 09:55
@grahamd
In short, instead of this:
$ python t.py 
<class '__main__.Test'> => <class '__main__.Test'>
<type 'weakproxy'> => <class '__main__.Test'>

I expected to get this (note the second line):
<class '__main__.Test'> => <class '__main__.Test'>
<type 'weakproxy'> => <type 'weakproxy'>

I pass the proxy to the test() function, and I expect the argument inside the function to be a proxy. And that works - the type of obj in test() is weakproxy. 
However, the test function calls obj.method(), and I expected the method to get a proxy of obj as well, but *it does not* (the type of self is now Test)!

The reason I though this is the same bug is that your method doesn't work on a proxy any more!

def __iadd__(self, value):
      # self is not longer proxy! it's actually actual class instance now.
      ...

Now, if I call the method like this:
Test.method(weakref.proxy(o)), then obviously the self argument of the method is a proxy.

So, the crux of the matter is in how obj.method is handled inside Python interpreter. I think the current behavior is wrong, and if it is fixed, I'm certain your problem is fixed as well.
msg199135 - (view) Author: Graham Dumpleton (grahamd) Date: 2013-10-07 10:06
The proxy is intended as a wrapper around an object, it is not intended to merge in some way with the wrapped object. The wrapped object shouldn't really ever be aware that it was being accessed via a proxy. Thus the expectation that the 'self' attribute of the methods of the wrapper object would actually be the proxy object is a strange one.

Can you explain why you need it to behave the way you are expecting? Also specifically indicate what requirement you have for needing the reference to the wrapped object to be a weakref?

Almost sounds a bit like you may be trying to use weakref.proxy in a way not intended.

It is technically possible to write an object proxy which would work how you are expecting, but the Python standard library doesn't provide an object proxy implementation to base such a thing on.
msg199138 - (view) Author: (shishkander) Date: 2013-10-07 11:10
@grahamd
Well, I read the PEP for weakproxy and python doc on weakref library, and I got an impression that weakproxy is really like a weak pointer in C++: i can pass it around as it was a strong pointer, and get an exception if actual object has been deleted. Well, but I see now that it was wrong.

My use case

If you are familiar with kazoo library, then what I am making is similar to KazooClient object. See here, for example:
https://kazoo.readthedocs.org/en/latest/basic_usage.html#connection-handling
The typical usage for my object is like this:

zk = KazooClient(hosts='127.0.0.1:2181')
zk.start()
# user *MUST* call zk.stop() otherwise there is a leak

Now I think it's not user-friendly to require the call to zk.stop(). I prefer the call to stop() to be called automatically from destructor of zk object whenever it goes out of scope. However, the background thread which is created in zk.start() also holds reference to zk object, and so __del__ is never called, the thread is never stopped, and the leak occurs.

Of course, I can do this to solve the problem above:

class Wrapper(object):
  def __init__(self, *args, **kwargs):
    self.__obj = KazooClient(*args, **kwargs)
    self.__obj.start()
  def __getattr__(self, attr):
    return getattr(self.__obj, attr)
  def __del__(self):
    self.__obj.stop()


But even this doesn't quite solve the problem of callbacks. To have callbacks from KazooClient, they have to be stored in Wrapper object instead, and KazooClient has to be given weak references to some special Wrapper method, which will call the real callback. When KazooClient executes callback, it operates on a weakproxy, but as soon as it calls wrapper.method() the weakproxy is transformed to actual instance, and that complicates things.

It still works with current weakref implementation, but it would be simpler and more flexible if the weakproxy was working as I expected, as then I don't have to use unnatural calls: Class.method(proxy, ...) instead of just proxy.method(...).
msg199141 - (view) Author: Graham Dumpleton (grahamd) Date: 2013-10-07 11:21
The __del__() method is generally something to be avoided. As this is a design issue with how you are doing things, I would suggest you move the discussion to:

https://groups.google.com/forum/#!forum/comp.lang.python

You will no doubt get many suggestions there.
History
Date User Action Args
2020-11-15 19:31:24iritkatrielsetversions: + Python 3.8, Python 3.9, Python 3.10, - Python 2.6, Python 2.7, Python 3.3
2013-10-07 11:21:21grahamdsetmessages: + msg199141
2013-10-07 11:10:35shishkandersetmessages: + msg199138
2013-10-07 10:06:22grahamdsetmessages: + msg199135
2013-10-07 09:55:05shishkandersetmessages: + msg199133
2013-10-07 08:22:35grahamdsetmessages: + msg199132
2013-10-07 06:10:30shishkandersetfiles: + t.py
nosy: + shishkander
messages: + msg199130

2013-09-22 11:23:57ncoghlansetmessages: + msg198262
2013-09-22 10:42:12pitrousetnosy: + tim.peters, ncoghlan
2013-09-22 10:35:43grahamdcreate