Issue23990
This issue tracker has been migrated to GitHub,
and is currently read-only.
For more information,
see the GitHub FAQs in the Python's Developer Guide.
Created on 2015-04-17 18:52 by ionelmc, last changed 2022-04-11 14:58 by admin. This issue is now closed.
Files | ||||
---|---|---|---|---|
File name | Uploaded | Description | Edit | |
callable.diff | llllllllll, 2015-04-17 22:52 | review | ||
callable2.diff | ionelmc, 2016-05-23 15:39 | addresses Eric Snow's comment | review |
Messages (81) | |||
---|---|---|---|
msg241352 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 18:52 | |
It appears that callable doesn't really care for the descriptor protocol, so it return True even if __call__ is actually an descriptor that raise AttributeError (clearly not callable at all). Eg: Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 24 2015, 22:44:40) [MSC v.1600 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> callable <built-in function callable> >>> class A: ... @property ... def __call__(self): ... raise AttributeError('go away') ... >>> a = A() >>> a <__main__.A object at 0x000000000365B5C0> >>> a.__call__ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __call__ AttributeError: go away >>> callable(a) True >>> # it should be False :( |
|||
msg241353 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 19:22 | |
For context this respects the descriptor protocol: >>> a() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __call__ AttributeError: go away Mind you, this is legal use: >>> class B: ... @property ... def __call__(self): ... return lambda: 1 ... >>> b = B() >>> b() 1 |
|||
msg241354 - (view) | Author: Christian Heimes (christian.heimes) * | Date: 2015-04-17 19:30 | |
callable() just checks that an object can be called. It doesn't check if the actual call or even the access to the __call__() member succeeds. Magic methods are resolved on the class or type of an object, not the object itself. Therefore the descriptor protocol is not invoked unless you do the actual call. The check roughly translated to: def callable(obj): return hasattr(type(obj), '__call__') |
|||
msg241355 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 19:38 | |
Christian, it's not clear why you're closing this. You basically just described the current broken behaviour. That by itself is not a reason enough (it's a sort of circular argument ;-). Can you please explain what prevents this to be fixed or how we got this implementation in the first place? From my perspective the current behavior is inconsistent, as outlined in the above examples. |
|||
msg241358 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-17 20:01 | |
I am also confused by this; I would imagine that callable(obj) would respect the descriptor protocol. I have a proposed patch that would make this work with descriptors. |
|||
msg241360 - (view) | Author: Christian Heimes (christian.heimes) * | Date: 2015-04-17 20:14 | |
I have closed the issue because the code behaves according to the language specs and the language design. It is not broken at all. The callable test just checks for the attribute __call__ on the *type* of an object. The check is not performed on the *object* itself. In your example callable(a) does not do hasattr(a, '__call__') but hasattr(type(a), '__call__') which translates to hasattr(A, '__call__') The behavior is very well consistent. I presume it just doesn't match your expectations. Special methods have a slightly different lookup behavior than ordinary. Due to the highly dynamic nature of Python the __call__ attribute is not validated at all. For example this is expected behavior: >>> class Example: ... __call__ = None ... >>> callable(Example()) True >>> class Example: ... __call__ = None ... >>> example = Example() >>> callable(example) True >>> example() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NoneType' object is not callable |
|||
msg241361 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-17 20:18 | |
As Christian Heimes explained, this is not a bug. Please do not reopen it. |
|||
msg241362 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 20:22 | |
The docs do not explain that the check is performed against type(a). Nor that makes any sense - it's not very explicit. AFAIK Python doesn't have any specification, the behaviour is defined by cpython implementation, and I hope that you realise that any "it's not in the spec" argument is a circular one. |
|||
msg241364 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-17 20:32 | |
See: https://docs.python.org/3/reference/datamodel.html#special-method-names and https://docs.python.org/3/reference/datamodel.html#object.__getattribute__ and https://docs.python.org/3/reference/datamodel.html#special-lookup |
|||
msg241365 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 20:41 | |
Ethan, those sections you have linked to have nothing to do with special methods that are descriptors, or behaviour regarding descriptors. As seen in this example, descriptors are working even for special methods: >>> class B: ... @property ... def __call__(self): ... return lambda: 1 ... >>> b = B() >>> b() 1 |
|||
msg241371 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 22:42 | |
Some more discussions happened here: https://mail.python.org/pipermail/python-ideas/2015-April/033027.html So for Christian or Ethan, can you reconsider this issue? Joe has already kindly provided a patch for this that just does one extra check. It still checks the type of the object first. |
|||
msg241373 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-17 22:52 | |
Oops, I messed up the test case; here is a fixed version (the class name was wrong). Just a note: all the existing test cases passed AND the one proposed in this thread. I understand that it is currently working as intended; however, the argument is that the intended behaviour should be changed. |
|||
msg241375 - (view) | Author: Alexander Belopolsky (belopolsky) * | Date: 2015-04-17 23:00 | |
From <https://mail.python.org/pipermail/python-ideas/2015-April/033018.html>: >>>>>>> GvR <<<<<<<<<< I think you've found an unintended and undocumented backdoor. I admit I don't understand how this works in CPython. Overloaded operators like __add__ or __call__ should be methods in the class, and we don't look for them in the instance. But somehow defining them with @property works (I guess because @property is in the class). What's different for __call__ is that callable() exists. And this is probably why I exorcised it Python 3.0 -- but apparently it's back. :-( In the end callable() doesn't always produce a correct answer; but maybe we can make it work in this case by first testing the class and then the instance? Something like (untested): def callable(x): return hasattr(x.__class__, '__call__') and hasattr(x, '__call__') >>>>>>> GvR <<<<<<<<<< |
|||
msg241377 - (view) | Author: Alexander Belopolsky (belopolsky) * | Date: 2015-04-17 23:07 | |
> But somehow defining them with @property works (I guess because @property is in the class). IMO, this is the bug and not the callable() behavior. |
|||
msg241379 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-17 23:13 | |
I don't think that using a property to define a callable in the class is a bug; while it seems less ideal, I don't understand why it would be unsupported with callable when it executes correctly. |
|||
msg241380 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 23:15 | |
> > But somehow defining them with @property works (I guess because @property is in the class). > IMO, this is the bug and not the callable() behavior. By the same reasoning we shouldn't be able to use staticmethod or classmethod for special methods. I'm pretty sure that's intended, even if it looks useless. |
|||
msg241381 - (view) | Author: James Edwards (jedwards) * | Date: 2015-04-17 23:28 | |
It seems like this issue has morphed over time. At the beginning, it looked like you expected perfectly reasonable (but odd) definitions of __call__ attributes, where the logic inside raised an Exception, to be somehow determined to be "uncallable". This does not seem realistic at all. Then it turned into detecting whether the __call__ attribute was None or not. Fine, I guess. But towards the end, it has turned into you wanting to check whether both an instance and a class have __call__ attributes. Which doesn't seem like too much of an unreasonable change. But these changes and the proposed patch won't address your initial issue of somehow detecting that the logic inside will raise an AttributeError or that __call__ is defined, but None. |
|||
msg241383 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-17 23:31 | |
Actually it does address it, as AttributeError is very special: >>> a.__call__ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __call__ AttributeError: go away >>> hasattr(a, '__call__') False |
|||
msg241384 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-18 00:09 | |
As ionelmc mentioned, it does address the issue proposed originally and in the patch this is added as another test case for the callable function |
|||
msg241386 - (view) | Author: Terry J. Reedy (terry.reedy) * | Date: 2015-04-18 00:11 | |
__call__ is a reserved name defined to be an instance method. I view wrapping *any* reserved-name instance method with @property to be a bug in that it redefines the name as a data attribute implemented with a 'hidden' get method. A function wrapped with @staticmethod or @classmethod is also not an instance method. The doc for callable says " If this returns true, it is still possible that a call fails," In your example, calling a() fails with AttributeError, with or without the @property decoration. Either way it does not contradict the doc. The doc also says "but if it is false, calling object will never succeed." So if you can define an object f such that callable(f) == False and print(f()) prints something, that would show a bug (and be a test case). Failing that, this is appears to be a feature request, not a bug fix. |
|||
msg241387 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-18 00:14 | |
I don't see how it is a bug that you can make __call__ an arbitrary descriptor as long as it returns a valid callable. if n.__call__ is a valid callable, why should it matter that it was looked up as a descriptor or as an instancemethod? |
|||
msg241388 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-18 01:19 | |
The purpose of callable is to report whether an instance is callable or not, and that information is available on the instance's class, via the presence of __call__. It is not up to callable() nor iter() nor ... to figure out that, even though the special method __call__ or __iter__ or ... exist, the object isn't /really/ what it says it is. If you have special needs then write special functions, and they can be imported and used instead of the regular built-in ones. |
|||
msg241389 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-18 01:28 | |
"The purpose of callable is to report whether an instance is callable or not" I am totally with you so far until you get to: "and that information is available on the instance's class, via the presence of __call__". I don't understand why this assumption must be made. The following class is totally valid and callable (in the sense that I can use function call syntax on instances): class C(object): @property def __call__(self): return lambda: None Also, I don't understand why you would mention __iter__, __iter__ respects the descriptor protocol also: >>> class C(object): ... @property ... def __iter__(self): ... return lambda: iter(range(10)) ... >>> it = iter(C()) >>> next(it) 0 >>> next(it) 1 >>> next(it) 2 |
|||
msg241397 - (view) | Author: Terry J. Reedy (terry.reedy) * | Date: 2015-04-18 04:45 | |
To extend msg241386: A bug, for this tracker, is a discrepancy between specification and behavior. Wrapping an instance method with @property changes its behavior so that it no longer meets the specification for an 'instance method'. (PS to Joe: please leave the headers alone.) |
|||
msg241398 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-18 05:31 | |
Python 3.4.0 (default, Apr 11 2014, 13:05:18) [GCC 4.8.2] on linux Type "help", "copyright", "credits" or "license" for more information. --> class NonIter: ... pass ... --> list(iter(NonIter())) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NonIter' object is not iterable --> class MaybeIter: ... @property ... def __next__(self): ... raise AttributeError ... def __iter__(self): ... return self ... --> iter(MaybeIter()) <__main__.MaybeIter object at 0xb6a5da2c> # seems to work --> list(iter(MaybeIter())) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __next__ AttributeError This is exactly analogous to what you are seeing with __call__ and callable(). I am not opposed to "fixing" callable(), I'm opposed to fixng only callable(). If you want to have the descriptor protocol be given more weight in dealing with special methods (aka magic methods aka dunder methods) then be thorough. Find all (or at least most ;) of the bits and pieces that rely on these __xxx__ methods, write a PEP explaining why this extra effort should be undertaken, get a reference implementation together so the performance hit can be measured, and then make the proposal. |
|||
msg241407 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-18 10:30 | |
> This is exactly analogous to what you are seeing with __call__ and callable(). Your example is incorrect, __next__ is what makes an object iterable but not what makes an object have an iterator (what __iter__ does). This correctly characterises the issue: >>> class NonIter: ... pass ... >>> iter(NonIter()) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NonIter' object is not iterable >>> >>> class DynamicNonIter: ... has_iter = False ... ... @property ... def __iter__(self): ... if self.has_iter: ... from functools import partial ... return partial(iter, [1, 2, 3]) ... else: ... raise AttributeError("Not really ...") ... >>> dni = DynamicNonIter() >>> iter(dni) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'DynamicNonIter' object is not iterable >>> dni.has_iter = True >>> iter(dni) <list_iterator object at 0x000000000362FF60> Now, if this is possible for `iter`, why shouldn't it be possible for `callable`? I'm not opposed to writing a PEP but the issue with `callable` is the only one I'm aware of, and this seems too small in scope anyway. |
|||
msg241423 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-18 16:23 | |
Your example shows /having/ an iterator, while mine is /being/ an iterator. A simple iterator: # iterator protocol class uc_iter(): def __init__(self, text): self.text = text self.index = 0 def __iter__(self): return self def __next__(self): try: result = self.text[self.index].upper() except IndexError: raise StopIteration self.index += 1 return result ucii = uc_iter('abc') I believe your over-arching goal is a proxy class? class GenericProxy: def __init__(self, proxied): self.proxied = proxied # in case proxied is an __iter__ iterator @property def __iter__(self): if not hasattr(self.proxied, '__iter__'): raise AttributeError else: return self @property def __next__(self): if not hasattr(self.proxied, '__next__'): raise AttributeError else: return next(self.proxied) and then two proxies to test -- a non-iterable and an iterable: gp_ni = GenericProxy(object()) gp_ucii = GenericProxy(ucii) and a quick harness: try: for _ in iter(gp_ni): print(_) except Exception as e: print(e) try: for _ in iter(gp_ucii): print(_) except Exception as e: print(e) Note: the presence/absence of iter() makes no difference to the results below. The non-iterable gives the correct error: 'GenericProxy' object is not iterable But the iterable gives: 'GenericProxy' object is not callable That error message is a result of the iter machinery grabbing the __next__ attribute and trying to call it, but property attributes are not callable. In other words, iter() does not "honor the descriptor protocol". So now we have two: callable() and iter(). How many more are there? |
|||
msg241425 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-18 16:44 | |
On Sat, Apr 18, 2015 at 7:23 PM, Ethan Furman <report@bugs.python.org> wrote: > > class GenericProxy: > def __init__(self, proxied): > self.proxied = proxied > # in case proxied is an __iter__ iterator > @property > def __iter__(self): > if not hasattr(self.proxied, '__iter__'): > raise AttributeError > else: > return self > @property > def __next__(self): > if not hasattr(self.proxied, '__next__'): > raise AttributeError > else: > return next(self.proxied) Unfortunately your implementation is incorrect as you forgot to that the property needs to return a function. This is a correct implementation that works as expected (in the sense that *iter does in fact honor the descriptor protocol)*: class GenericProxy: > def __init__(self, proxied): > self.proxied = proxied > # in case proxied is an __iter__ iterator > @property > def __iter__(self): > if not hasattr(self.proxied, '__iter__'): > raise AttributeError > else: > return *lambda:* self > @property > def __next__(self): > if not hasattr(self.proxied, '__next__'): > raise AttributeError > else: > return *lambda: *next(self.proxied) > The iter machinery doesn't "grab values and call them", you've misinterpreted the error. Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241426 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-18 16:47 | |
Turns out I've replied through email, and code got mangled. This is the correct version: class GenericProxy: def __init__(self, proxied): self.proxied = proxied @property def __iter__(self): if not hasattr(self.proxied, '__iter__'): raise AttributeError else: return lambda: self @property def __next__(self): if not hasattr(self.proxied, '__next__'): raise AttributeError else: return lambda: next(self.proxied) Note the lambdas. |
|||
msg241430 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-18 17:16 | |
I am happy to be proven wrong. :) |
|||
msg241431 - (view) | Author: Christian Heimes (christian.heimes) * | Date: 2015-04-18 17:35 | |
All major Python implementation have a mutual agreement that callable() just checks for a __call__ member on the type. You also haven't shown that this behavior violates the documentation and language spec. The check for existence of __call__ on the type is well in the range of expected behavior. I as well as other core devs had the same gut feeling. Finally your suggestion makes the implementation of callable() more complex and much, much slower. Right now and for more than a decade it is a simple, fast and straight forward code path for new style classes. It just requires two pointer derefs and one comparison to NULL. I'm still -1 on the change. If you want to go forth with your request then you must write a PEP and convince all implementors of Python implementations to change the current way callable() and other protocols like iter work. $ python2.7 Python 2.7.8 (default, Nov 10 2014, 08:19:18) [GCC 4.9.2 20141101 (Red Hat 4.9.2-1)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> class A(object): ... @property ... def __call__(self): ... raise AttributeError ... >>> a = A() >>> print(callable(a)) True >>> a() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __call__ AttributeError $ jython Jython 2.7b3+ (, Nov 3 2014, 11:02:14) [OpenJDK 64-Bit Server VM (Oracle Corporation)] on java1.8.0_40 Type "help", "copyright", "credits" or "license" for more information. >>> class A(object): ... @property ... def __call__(self): ... raise AttributeError ... >>> a = A() >>> print(callable(a)) True >>> a() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __call__ AttributeError $ pypy Python 2.7.8 (a980ebb26592ed26706cd33a4e05eb45b5d3ea09, Sep 24 2014, 07:41:52) [PyPy 2.4.0 with GCC 4.9.1 20140912 (Red Hat 4.9.1-9)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>>> class A(object): .... @property .... def __call__(self): .... raise AttributeError .... >>>> a = A() >>>> print(callable(a)) True >>>> a() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __call__ AttributeError |
|||
msg241433 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-18 17:43 | |
On Sat, Apr 18, 2015 at 8:35 PM, Christian Heimes <report@bugs.python.org> wrote: > You also haven't shown that this behavior violates the documentation and > language spec. How can I show it violates the "spec" when there's not such thing? :-) AFAIK, `callable` is not specified in any PEP. Please give some references when you make such statements. [...] write a PEP and convince all implementors of Python implementations > to change the current way callable() and other protocols like iter work. `iter` works fine, as outlined above? Am I missing something? What other protocols do you have in mind, wrt honoring descriptors? Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241435 - (view) | Author: Christian Heimes (christian.heimes) * | Date: 2015-04-18 18:00 | |
On 2015-04-18 19:43, Ionel Cristian Mărieș wrote: > > Ionel Cristian Mărieș added the comment: > > On Sat, Apr 18, 2015 at 8:35 PM, Christian Heimes <report@bugs.python.org> > wrote: > >> You also haven't shown that this behavior violates the documentation and >> language spec. > > How can I show it violates the "spec" when there's not such thing? :-) > AFAIK, `callable` is not specified in any PEP. Please give some references > when you make such statements. The language specs is made up from two things: 1) the CPython reference implemention 2) the documentation of the CPython reference implementation 3) accepted and implemented PEPs If 3) doesn't exist and 2) doesn't contain any point that contradicts 1), then 1) and 2) agree on an implementation and therefore it is informally specified. The fact that PyPy and Jython show the same behavior, adds additional weight to 1), too. callable() has been implemented like that since the introduction of new style classes, too. $ hg checkout v2.2 $ grep -A20 ^PyCallable_Check Objects/object.c PyCallable_Check(PyObject *x) { if (x == NULL) return 0; if (PyInstance_Check(x)) { PyObject *call = PyObject_GetAttrString(x, "__call__"); if (call == NULL) { PyErr_Clear(); return 0; } /* Could test recursively but don't, for fear of endless recursion if some joker sets self.__call__ = self */ Py_DECREF(call); return 1; } else { return x->ob_type->tp_call != NULL; } } You really have to make a *very* good case with a PEP in order to get everybody to switch to a different behavior! |
|||
msg241440 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2015-04-18 18:52 | |
I understand Ionel's point, and it is indeed 'callable' that is the outlier here. It only looks for the *existence* of the attribute, rather than actually retrieving it through the descriptor protocol (and therefore getting the AttributeError from the property). Protocols like iter, on the other hand, actually use the attribute, and therefore do access it via the descriptor protocol, and properties therefore act like one would naively expect them to. That doesn't mean we should definitely change callable, but it does mean the idea isn't obviously wrong. IMO it is indeed callable that has the surprising behavior here. (Note: I did not think that at first, but I do after reading the iter discussion. (NB: in general, __iter__ should *not* return self; that's a bug waiting to happen.)) But, callable is also...not exactly Pythonic at its core, as evidenced by Guido's desire to get rid of it. So the fact that it is in some sense buggy or at least surprising is perhaps something that we just live with because of that. IMO, code that depends on a __call__ property is a bit suspect anyway. Something should be callable or not, not conditionally callable. If a program wants things to be conditionally callable, the program can establish its own protocol for that. (The same applies to __iter__, but there's no reason to intentionally break the fact that it does work, since it just falls out of how the language works.) So I guess my feeling is...callable is buggy, but callable is a buggy API :) So I'm 0 on this issue (not even +0 or -0). |
|||
msg241450 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-18 19:21 | |
Perhaps callable() should be in the inspect module? ;) Speaking of which, how do all the is... functions there work with this descriptor implementation? |
|||
msg241451 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2015-04-18 19:24 | |
Oops, I accidentally changed the bug status due to not refreshing before I posted. |
|||
msg241452 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2015-04-18 19:33 | |
They use isinstance, except for a couple that also check co_flags, and the ones that check if the object is a descriptor. I haven't thought this through fully, but I think this means that in general the descriptor protocol has been invoked or not by the caller of inspect before inspect checks the object. There is no 'callable' type in python, so the closest analog in the inspect module to 'callable' are the functions that look for __get__ and __set__ methods on descriptors. If one of *those* is a descriptor, my head will start hurting :). |
|||
msg241524 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-19 18:01 | |
Note that (in my mind, unfortunately) the pickle module looks up several dunder methods on instances. That isn't quite the same thing since the issue is about callable not triggering the descriptor protocol. However it is closely related. I point this out because this similar behavior of pickle is a source of mysterious, hard to understand bugs in many of the same corner cases. Thus I'm -1 on changing the behavior of callable. From what I understand, the motivation here is in the case of proxies that dynamically determine their capability and raise AttributeError accordingly. If that is the case then consider prior art such as MagicMock or other existing proxy types on PyPI. Also consider if there is a proxy-specific approach that can resolve the matter. I've long thought there is room in the stdlib for a "proxy" module that holds proxy-related helpers and classes. Finally, instead of changing callable, why not use a metaclass that does the right thing? I believe MagicMock does something along those lines. |
|||
msg241530 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-19 18:28 | |
On Sun, Apr 19, 2015 at 9:01 PM, Eric Snow <report@bugs.python.org> wrote: > Finally, instead of changing callable, why not use a metaclass that does > the right thing? I believe MagicMock does something along those lines. > What would be the "right thing"? AFAIK this cannot be achieved with a metaclass, since the check in `callable` just does a dumb check for `x->ob_type->tp_call`. Seriously, if you know a way to make `x->ob_type->tp_call` run any python code please let me know :-) Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241540 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2015-04-19 19:01 | |
The "right thing", using a meta-class, is to have the meta-class check if the proxied object is callable, and if so, put in the __call__ function in the class that is being created. |
|||
msg241542 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-19 19:05 | |
On Sun, Apr 19, 2015 at 10:01 PM, Ethan Furman <report@bugs.python.org> wrote: > The "right thing", using a meta-class, is to have the meta-class check if > the proxied object is callable, and if so, put in the __call__ function in > the class that is being created. Yes indeed, for a plain proxy. Unfortunately for a *lazy* proxy this is not acceptable as it would "create" (or "access") the target. The point is to delay that till it's actually needed, not when the proxy is created. Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241554 - (view) | Author: Steven D'Aprano (steven.daprano) * | Date: 2015-04-19 19:29 | |
This bug report seems to be completely based on a false premise. In the very first message of this issue, Ionel says: "it return True even if __call__ is actually an descriptor that raise AttributeError (clearly not callable at all)." but that is wrong. It *is* callable, and callable() is correct to return True. If you look at the stack trace, the __call__ method (function/property, whatever you want to call it) is called, and it raises an exception. That is no different from any other method or function that raises an exception. It is wrong to think that raising AttributeError *inside* __call__ makes the object non-callable. Ionel, I raised these issues on Python-list here: https://mail.python.org/pipermail/python-ideas/2015-April/033078.html but you haven't responded to them. |
|||
msg241557 - (view) | Author: Joe Jevnik (llllllllll) * | Date: 2015-04-19 19:36 | |
This is a different case from raising an AttributeError inside the __call__; >>> class C(object): ... def __call__(self): ... raise AttributeError() ... >>> hasattr(C(), '__call__') True >>> class D(object): ... @property ... def __call__(self): ... raise AttributeError() ... >>> hasattr(C(), '__call__') False AttributeError was picked very intentionally for the example. The docs show that n(args) == n.__call__(args) if n has a __call__; however, if a property raises an AttributeError, then it really does not have a __call__. |
|||
msg241562 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-19 20:12 | |
On Sun, Apr 19, 2015 at 10:29 PM, Steven D'Aprano <report@bugs.python.org> wrote: > This bug report seems to be completely based on a false premise. In the > very first message of this issue, Ionel says: > > "it return True even if __call__ is actually an descriptor that raise > AttributeError (clearly not callable at all)." > > but that is wrong. It *is* callable, and callable() is correct to return > True. If you look at the stack trace, the __call__ method > (function/property, whatever you want to call it) is called, and it raises > an exception. That is no different from any other method or function that > raises an exception. > > It is wrong to think that raising AttributeError *inside* __call__ makes > the object non-callable. > > Ionel, I raised these issues on Python-list here: > > https://mail.python.org/pipermail/python-ideas/2015-April/033078.html > > but you haven't responded to them. I was hoping my other replies had addressed those issues. Note that the presence of __call__ on the callstack is merely an artefact of how @property works, and it's not actually the __call__ method (it's just something that property.__get__ calls). Here's an example that hopefully illustrates the issue more clearly: >>> class CallDescriptor: ... def __get__(self, inst, owner): ... target = inst._get_target() ... if callable(target): ... return target ... else: ... raise AttributeError('not callable') ... >>> class LazyProxy: ... __call__ = CallDescriptor() ... def __init__(self, get_target): ... self._get_target = get_target ... >>> def create_stuff(): ... # heavy computation here ... print("Doing heavey computation!!!!") ... return 1, 2, 3 ... >>> proxy = LazyProxy(create_stuff) >>> callable(proxy) ################### this should be false! True >>> hasattr(proxy, '__call__') Doing heavey computation!!!! False >>> >>> def create_callable_stuff(): ... # heavy computation here ... print("Doing heavey computation!!!!") ... def foobar(): ... pass ... return foobar ... >>> proxy = LazyProxy(create_callable_stuff) >>> callable(proxy) True >>> hasattr(proxy, '__call__') Doing heavey computation!!!! True Now it appears there's a second issue, slightly related - if you actually call the proxy object AttributeError is raised (instead of the TypeError): >>> proxy = LazyProxy(create_stuff) >>> callable(proxy) True >>> hasattr(proxy, '__call__') Doing heavey computation!!!! False >>> proxy() Doing heavey computation!!!! Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in __get__ AttributeError: not callable >>> >>> target = create_stuff() Doing heavey computation!!!! >>> target() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object is not callable Contrast that to how iter works - if the descriptor raise AttributeError then iter raises TypeError (as expected): >>> class IterDescriptor: ... def __get__(self, inst, owner): ... target = inst._get_target() ... if hasattr(type(target), '__iter__') and hasattr(target, '__iter__'): ... return target.__iter__ ... else: ... raise AttributeError('not iterable') ... >>> class LazyProxy: ... __iter__ = IterDescriptor() ... def __init__(self, get_target): ... self._get_target = get_target ... >>> def create_iterable_stuff(): ... # heavy computation here ... print("Doing heavey computation!!!!") ... return 1, 2, 3 ... >>> proxy = LazyProxy(create_iterable_stuff) >>> iter(proxy) Doing heavey computation!!!! <tuple_iterator object at 0x0000000002B7C908> >>> >>> def create_noniterable_stuff(): ... # heavy computation here ... print("Doing heavey computation!!!!") ... def foobar(): ... pass ... return foobar ... >>> proxy = LazyProxy(create_noniterable_stuff) >>> iter(proxy) Doing heavey computation!!!! Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'LazyProxy' object is not iterable >>> >>> proxy.__iter__ Doing heavey computation!!!! Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in __get__ AttributeError: not iterable So this is why I'm bringing this up. If `iter` wouldn't handle it like that then I'd think that maybe this is the intended behaviour. I hope the blatant inconsistency is more clear now , and you'll understand that this bug report is not just some flagrant misunderstanding of how __call__ works. To sum this up, the root of this issue is that `callable` doesn't do all the checks that are done right before actually performing the call (like the descriptor handling). It's like calling your doctor for an appointment where the secretary schedules you, but forgets to check if the doctor is in vacation or not. Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241568 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-19 22:09 | |
> What would be the "right thing"? My suggestion of using a metaclass is actually not effective here because __call__ has meaning for metaclasses. Otherwise you could have made __call__ more dynamic via a metaclass. Except that is another reason why my suggestion is incorrect. What you are asking for is that, effectively, the state of the instance might be able to impact the resolution of special methods of a class. So a metaclass approach would not have helped since the instance would not have been involved in the lookup. Regardless, this makes it more clear to me what you are trying to accomplish for the sake of a proxy type. The question is, should an instance be able to influence the resolution of special methods that are called on its behalf? Before answering that, consider why methods that are special to the interpreter get treated differently. The language specifies that rather than using obj.__getattribute__ to look up special methods, they are effectively located in type(obj).__dict__. They likewise are not looked up on obj.__class__.__dict__. Here are the key reasons why this matters: * speed * the special methods will still be available even if the class implements its own __getattribute__ Once the methods are looked up, the descriptor protocol is invoked, if applicable. However, it does not fall back to obj.__getattr__. See Objects/typeobject.c:_PyObject_LookupSpecial. So ultimately the descriptor protocol allows instances to have a say in both the existence and the behavior of special methods. However, I think that the former is unfortunate since it obviously muddies the water here. I doubt it was intentional. Back to the question, should instances be allowed to influence the *lookup* of special methods? Your request is that they should be and consistently. As noted, the interpreter already uses the equivalent of the following when looking up special methods: def _PyObject_LookupSpecial(obj, name): attr = inspect.getattr_static(obj, name) try: f = inspect.getattr_static(attr, '__get__') except AttributeError: return attr else: return f(attr, obj, type(obj)) What you are asking is that callable should do this too, rather than skipping the descriptor part). I expect the same would need to be done for any other helper that also checks for "special" capability. For example, see the various __subclasshook__ implementations in Lib/_collections_abc.py. We should be consistent about it if we are going to do it. |
|||
msg241569 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-19 22:10 | |
Just to be clear, I'm still -1 on any of this. On the one hand, there's a risk of backward-compatibility breakage (just as much a corner-case as the need expressed in this issue). On the other hand, I'd actually push for _PyObject_LookupSpecial to be fixed to chain AttributeError coming from a descriptor into a TypeError. Allowing instances to determine the capability of a class feels wrong and potentially broken. Furthermore, doing so via AttributeError is problematic since it may mask an AttributeError that bubbles up (which is very confusing and hard to debug). I've been bitten by this with pickle. Still, it may be a good idea to expose _PyObject_LookupSpecial via the inspect module, but that should be addressed in a separate issue. |
|||
msg241570 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-19 22:12 | |
s/TypeError/RuntimeError/ |
|||
msg241571 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-19 22:33 | |
I want to address the four main points of criticism in fixing this issue, just in case it's not clear why I think those lines of thought are wrong: #1. "It's specified/documented, therefore it's intended" The first thing a maintainer does is check the docs. This is a sensible thing to do - as you cannot have all the details in your hear. The main question at that point: "is it really like that?". However, it's easy to miss the fact that the documentation explains an implementation issue (`callable` is not really reliable, blablabla), and not the intent of `callable`. I mean, the name is pretty clear on what it's supposed to do: "is the object callable or not?" - simple as that. If the intent of `callable` is being unreliable then maybe we should just rename it to `maybe_callable` or `unreliable_callable`, or maybe even "crappy_callable_we_dont_want_to_fix". #2. "But the call could fail anyway, so what's the point of fixing this?" The problem with this argument is that it's the same argument people bring up to remove the `callable` builtin. The problem is that you wouldn't use `callable` at all if you can just try/call/except. So this argument is not pertinent to the problem at hand (`callable` doing incomplete checks). #3. "But it's going to be too slow!" I don't want to be mean here, but this is just FUD. Lets measure this first. Is there really a measurable and significant performance impact on major applications? Does the argument even make sense in theory? A function call is pretty expensive in python, a mere attribute lookup wouldn't increase the cost by an order of magnitude (like 10x), would it? > py -3 -mtimeit -s "def foo(): pass" "foo.__call__" 10000000 loops, best of 3: 0.0585 usec per loop > py -3 -mtimeit -s "def foo(): pass" "callable(foo)" 10000000 loops, best of 3: 0.0392 usec per loop Is this a significant margin? Also, I'm pretty sure those numbers can be improved. Python 3 regressed performance in various aspects (and improved other things, of course), why would this be a roadblock now? #4. "It's too tricky, and I had a bad time with pickle one time ago", or: Exception masking issues This is certainly a problem, but it's not a new problem. There are already dozens of places where AttributeError is masked into something else (like a TypeError, or just a different result). Were do we draw the line here? Do we want to eventually get rid of all exception masking in an eventual Python 4.0 - what's the overarching goal here? Or is this just one of the many quirks of Python? What's worse - a quirk or a inconsistent quirk? The problem with this argument is that it attacks a different problem, that's just being made more visible if and when this problem of `callable` is fixed. Lets consider this strawman here: if a an user writes code like this: try: do important stuff except: pass # have fun debugging, haha who's at fault here? Is it the user that wrote that debugging black hole or is it python for letting the user do things like that? I don't think it's reasonable for Python to prevent exception masking bugs if the user was brave enough to write a descriptor. |
|||
msg241573 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-19 23:59 | |
> Ionel Cristian Mărieș added the comment: > #1. "It's specified/documented, therefore it's intended" > > The first thing a maintainer does is check the docs. This is a sensible thing to do - as you cannot have all the details in your hear. The main question at that point: "is it really like that?". > > However, it's easy to miss the fact that the documentation explains an implementation issue (`callable` is not really reliable, blablabla), and not the intent of `callable`. > > I mean, the name is pretty clear on what it's supposed to do: "is the object callable or not?" - simple as that. If the intent of `callable` is being unreliable then maybe we should just rename it to `maybe_callable` or `unreliable_callable`, or maybe even "crappy_callable_we_dont_want_to_fix". "callable" refers to the compatibility of the object with the call syntax - simple as that. The call syntax is turned into a special lookup for "__call__" that explicitly skips lookup on the instance (in CPython it is a call to _PyObject_LookupSpecial) and then a call on the resulting value. You are correct that callable does not do the descriptor part of that lookup. However, that is consistent across Python and has been this way for a long time (so there are backward compatibility concerns that cannot be ignored). > > #2. "But the call could fail anyway, so what's the point of fixing this?" You are correct that the availability of __call__ is the only relevant issue here. > > #3. "But it's going to be too slow!" > > I don't want to be mean here, but this is just FUD. Lets measure this first. Is there really a measurable and significant performance impact on major applications? It's not just applications. Grep for "PyCallable_Check" in the CPython code base. > > Does the argument even make sense in theory? A function call is pretty expensive in python, a mere attribute lookup wouldn't increase the cost by an order of magnitude (like 10x), would it? A "mere attribute lookup" involves invoking the descriptor protocol. This would add a discernible performance impact and the possibility of running arbitrary code for the sake of a rare corner case. The overall impact would be small, especially considering the use of PyCallable_Check in the CPython code base, but do not assume it would be insignificant. > Python 3 regressed performance in various aspects (and improved other things, of course), why would this be a roadblock now? My apology for being so blunt, but that's a terrible rationale! Let's make it better not worse. > > #4. "It's too tricky, and I had a bad time with pickle one time ago", or: Exception masking issues > > This is certainly a problem, but it's not a new problem. There are already dozens of places where AttributeError is masked into something else (like a TypeError, or just a different result). It not a problem currently for callable. It is one you are proposing to introduce. It is one which current users of callable don't have to worry about. > > Were do we draw the line here? We don't add to the problem. Instead, we work to decrease it. > Do we want to eventually get rid of all exception masking in an eventual Python 4.0 - what's the overarching goal here? Or is this just one of the many quirks of Python? > > What's worse - a quirk or a inconsistent quirk? What's the quirk here? I'd argue that the quirk is that special method lookup (_PyObject_LookupSpecial) doesn't turn AttributeError from a getter into RuntimeError. |
|||
msg241580 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 00:41 | |
On Mon, Apr 20, 2015 at 2:59 AM, Eric Snow <report@bugs.python.org> wrote: > However, that is consistent across Python and has been this > way for a long time (so there are backward compatibility concerns that > cannot be ignored). > It's not. Did you see the example with iter()/__iter__? It does convert the AttributeError into a TypeError. |
|||
msg241581 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 00:50 | |
On Mon, Apr 20, 2015 at 2:59 AM, Eric Snow <report@bugs.python.org> wrote: > It not a problem currently for callable. It is one you are proposing > to introduce. It is one which current users of callable don't have to > worry about. > > > > > Were do we draw the line here? > > We don't add to the problem. Instead, we work to decrease it. > What exactly are you proposing? Getting rid of AttributeError masking? I'm talking about applying an old design decision (AttributeError masking) in `callable`. Doesn't seem useful to talk about not having exception making unless you have a plan to remove that from other places (that's even harder than fixing `callable` IMO) just to fix this inconsistent handling in Python. Unless you think having inconsistent handling is OK. I do not think it's OK. There should be the same rules for attribute access everywhere. |
|||
msg241586 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-20 01:43 | |
> Ionel Cristian Mărieș added the comment: > It's not. Did you see the example with iter()/__iter__? It does convert > the AttributeError into a TypeError. callable and iter are not the same thing though. callable checks for a capability. iter invokes a capability. The direct comparision would be collections.abc.Iterable.__subclasshook__ (e.g. isinstance(obj, Iterable)), which behaves exactly like callable does (does not invoke the descriptor protocol). See Lib/_collections_abc.py. |
|||
msg241587 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-20 01:50 | |
> Ionel Cristian Mărieș added the comment: > What exactly are you proposing? Getting rid of AttributeError masking? That isn't really a practical thing to consider, so no. :) Instead I'm suggesting there isn't a lot of justification to change the behavior of callable. *If* we were to change anything I'd suggest disallowing AttributeError from a descriptor's getter during special method lookup. However, I'm not suggesting that either. We should simply leave callable alone (and consistent with the other helpers that inspect the "special" *capability* of objects). |
|||
msg241597 - (view) | Author: Raymond Hettinger (rhettinger) * | Date: 2015-04-20 04:02 | |
Unless there are some serious objections, I propose to close this on the basis of "practicality beats purity" (and as David Murray noted, there may not be a pure answer). Eric Snow's comments are dead-on. AFAICT, there isn't a real problem here and the API for better-or-worse has proven to be usable in practice (the callable() API has been around practically forever and the descriptor protocol has been around since Py2.2 -- if there were a real issue here, it would have reared it head long ago). |
|||
msg241623 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 08:41 | |
On Mon, Apr 20, 2015 at 4:50 AM, Eric Snow <report@bugs.python.org> wrote: > We should > simply leave callable alone (and consistent with the other helpers > that inspect the "special" *capability* of objects). > Which are the other helpers? Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241624 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 08:43 | |
On Mon, Apr 20, 2015 at 7:02 AM, Raymond Hettinger <report@bugs.python.org> wrote: > AFAICT, there isn't a real problem here and the API for better-or-worse > has proven to be usable in practice (the callable() API has been around > practically forever and the descriptor protocol has been around since Py2.2 > -- if there were a real issue here, it would have reared it head long ago). I think this is largely a product of misunderstanding the issue. As you can see in the early comments in the thread, the fact that special methods use descriptors is really really obscure. Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241640 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2015-04-20 13:17 | |
The only 'consistency' fix that would make any sense, IMO, would be to disallow special methods to be descriptors. We can't do that for backward compatibility reasons, so that's pretty much case closed. Eric already mentioned one of the other 'capability' helpers: rdmurray@pydev:~/python/p35>./python Python 3.5.0a3+ (default:5612dc5e6af9+, Apr 16 2015, 11:29:58) [GCC 4.8.2] on linux Type "help", "copyright", "credits" or "license" for more information. >>> class Foo: ... @property ... def __iter__(self): ... raise AttributeError ... >>> f = Foo >>> from collections.abc import Iterable >>> issubclass(Foo, Iterable) True |
|||
msg241644 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2015-04-20 13:28 | |
I in case it wasn't clear, I closed this not because of my "case closed" statement, but because as Eric pointed out we *do* have consistency here: things which check *capabilities* (as opposed to actually *using* the special methods), like callable and Iterable, only look for the existence of the method, consistently. The fact that you can then get an AttributeError later when actually using the method is unfortunate, but there really isn't anything sensible to be done about it, due to backward compatibility concerns. |
|||
msg241645 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 13:36 | |
My point was about consistency in descriptor handling, not consistency of fault (eg: broken everywhere). I don't understand why that's not clear here. The big idea here is to harmonize capability checking with descriptor handling. Justifying breakage in callable with breakage in collections.Callable serves it no justice. On Monday, April 20, 2015, R. David Murray <report@bugs.python.org> wrote: > > R. David Murray added the comment: > > I in case it wasn't clear, I closed this not because of my "case closed" > statement, but because as Eric pointed out we *do* have consistency here: > things which check *capabilities* (as opposed to actually *using* the > special methods), like callable and Iterable, only look for the existence > of the method, consistently. The fact that you can then get an > AttributeError later when actually using the method is unfortunate, but > there really isn't anything sensible to be done about it, due to backward > compatibility concerns. > > ---------- > > _______________________________________ > Python tracker <report@bugs.python.org <javascript:;>> > <http://bugs.python.org/issue23990> > _______________________________________ > -- Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241648 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 13:45 | |
Also, descriptors are a core mechanism in new-style classes - you can't have methods without descriptors. Why would you even consider removing descriptors from the special method lookup if that's part of the object model design? On Monday, April 20, 2015, Ionel Cristian Mărieș <report@bugs.python.org> wrote: > > Ionel Cristian Mărieș added the comment: > > My point was about consistency in descriptor handling, not consistency of > fault (eg: broken everywhere). I don't understand why that's not clear > here. > > The big idea here is to harmonize capability checking with descriptor > handling. Justifying breakage in callable with breakage in > collections.Callable serves it no justice. > > On Monday, April 20, 2015, R. David Murray <report@bugs.python.org > <javascript:;>> wrote: > > > > > R. David Murray added the comment: > > > > I in case it wasn't clear, I closed this not because of my "case closed" > > statement, but because as Eric pointed out we *do* have consistency here: > > things which check *capabilities* (as opposed to actually *using* the > > special methods), like callable and Iterable, only look for the existence > > of the method, consistently. The fact that you can then get an > > AttributeError later when actually using the method is unfortunate, but > > there really isn't anything sensible to be done about it, due to backward > > compatibility concerns. > > > > ---------- > > > > _______________________________________ > > Python tracker <report@bugs.python.org <javascript:;> <javascript:;>> > > <http://bugs.python.org/issue23990> > > _______________________________________ > > > > -- > > Thanks, > -- Ionel Cristian Mărieș, http://blog.ionelmc.ro > > ---------- > > _______________________________________ > Python tracker <report@bugs.python.org <javascript:;>> > <http://bugs.python.org/issue23990> > _______________________________________ > -- Thanks, -- Ionel Cristian Mărieș, http://blog.ionelmc.ro |
|||
msg241649 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2015-04-20 13:53 | |
Because special methods are special. |
|||
msg241650 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-20 14:02 | |
> Ionel Cristian Mărieș added the comment: > Also, descriptors are a core mechanism in new-style classes - you can't > have methods without descriptors. Why would you even consider removing > descriptors from the special method lookup if that's part of the object > model design? Also, we are not changing anything here and we are not considering removing descriptors from special method lookup. This is the way it has been for a long time for code that *checks* for special method capability. As RDM and I have both said, changing that would break backward compatibility. As I've already explained, I also think it is wrong. |
|||
msg241651 - (view) | Author: Eric Snow (eric.snow) * | Date: 2015-04-20 14:08 | |
FYI, I'll re-iterate something I said before, there is a different approach you can take here if this is just an issue of proxying. Use two different proxy types depending on if the proxied object is callable or not: class Proxy: # all the proxy stuff... class CallableProxy(Proxy): def __call__(self, *args, **kwargs): ... def proxy(obj): if callable(obj): return CallableProxy(obj) else: return Proxy(obj) If that isn't a viable alternative then please explain your use case in more detail. I'm sure we can find a solution that works for you. |
|||
msg241653 - (view) | Author: Raymond Hettinger (rhettinger) * | Date: 2015-04-20 14:42 | |
> My point was about consistency in descriptor handling, not consistency > of fault (eg: broken everywhere). I don't understand why that's not > clear here. That is clear but also isn't sufficient motivation. The proposed change is unnecessary and not rooted in real use cases. It is a semantic change to a long-standing view that callable() means having a __call__ method. > The big idea here is to harmonize capability checking with descriptor handling. That isn't what Guido intended when he designed the capability checking. He has a remarkably good instinct for avoiding language complexity when there aren't clear-cut benefits. > http://blog.ionelmc.r Please stop using the bug tracker to post links to your blog. |
|||
msg241664 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2015-04-20 15:38 | |
On Mon, Apr 20, 2015 at 5:42 PM, Raymond Hettinger <report@bugs.python.org> wrote: > > That is clear but also isn't sufficient motivation. The proposed change > is unnecessary and not rooted in real use cases. It is a semantic change > to a long-standing view that callable() means having a __call__ method. > There is one use case: a lazy object proxy (there are some examples in the earlier replies). Eric proposed a CallableProxy/NonCallableProxy dichtonomy but I don't really like that API (feels wrong and verbose). > Please stop using the bug tracker to post li Sorry about that, I've replied through email and wasn't aware of bugtracker etiquette. The bugtracker doesn't have a nice way to reply to messages and it's atrocious on a mobile device. |
|||
msg241665 - (view) | Author: Eryk Sun (eryksun) * | Date: 2015-04-20 15:50 | |
To be consistent you'd have to do the attribute check in PyObject_Call as well, i.e. if an object isn't callable, then trying to call it should raise a TypeError. I think the cost can be mitigated by only checking heap types (i.e. tp_flags & Py_TPFLAGS_HEAPTYPE). It would be cleaner if slot_tp_call failed by raising TypeError instead of letting the AttributeError bubble up. There's a precedent in slot_tp_iter to raise a TypeError if lookup_method() fails. This would avoid having to change PyObject_Call. |
|||
msg266149 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2016-05-23 13:46 | |
On Mon, Apr 20, 2015 at 5:08 PM, Eric Snow <report@bugs.python.org> wrote: > FYI, I'll re-iterate something I said before, there is a different > approach you can take here if this is just an issue of proxying. Use two > different proxy types depending on if the proxied object is callable or not: > > > class Proxy: > # all the proxy stuff... > > > class CallableProxy(Proxy): > def __call__(self, *args, **kwargs): > ... > > > def proxy(obj): > if callable(obj): > return CallableProxy(obj) > else: > return Proxy(obj) > > > If that isn't a viable alternative then please explain your use case in > more detail. I'm sure we can find a solution that works for you. > This does not work if I need to resolve the `obj` at a later time (way later than the time I create the Proxy object). Unfortunately, I need exactly that. Not sure how that wasn't clear from all the examples I posted ... |
|||
msg266172 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2016-05-23 18:34 | |
To change this behavior at this point will require a PEP. The PEP should be something along the lines of "Add proxy support to builtins" and should address such things as callable, issubclass, and whatever else is is appropriate. As for working around your problem now I see a few options: - write your own `is_proxy_callable` function - add a `.callable` method to your proxy class - a `resolve_proxy` function/method which can change the proxy class type to whatever is needed to mirror the actual object |
|||
msg266177 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2016-05-23 19:49 | |
On Mon, May 23, 2016 at 9:34 PM, Ethan Furman <report@bugs.python.org> wrote: > "Add proxy support to builtins" and should address such things as > callable, issubclass, and whatever else is is appropriate. As previously stated this builtin is the only one not doing the right thing. If you think otherwise, please provide proof. I provided plenty of supporting examples. Your "working around" is basically saying "don't use callable or don't solve your problem". Not an option. Let me restate the problem: I want to implement a "proxy that resolves the target at a later time". That means I can't juggle classes ahead of time (no `resolve_proxy` or whatever) and I can't tell users "don't use callable" (what's the point of having a proxy then?). |
|||
msg266179 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2016-05-23 20:04 | |
`issubclass` doesn't "do the right thing", as evidenced by another dev. Continued complaining to this issue is not going to get you what you want, but writing a PEP to address the issue might. |
|||
msg266180 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2016-05-23 20:15 | |
On Mon, May 23, 2016 at 11:04 PM, Ethan Furman <report@bugs.python.org> wrote: > `issubclass` doesn't "do the right thing", as evidenced by another dev. > Hmmm. Technically, in that case, the problem is in collections.abc.Iterable, not issubclass. I can extent the patch to fix that too. Continued complaining to this issue is not going to get you what you want, > but writing a PEP to address the issue might. > Not sure what you're going at here. Are you hinting that you'd support these changes if there would be a PEP? |
|||
msg266181 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2016-05-23 20:21 | |
I'm not against them, but there are many diverse opinions. A PEP will: - broaden discussion, so all (or at least most) pros and cons can be considered - highlight any other pieces of Python that would need to change to properly support proxies - get a pronouncement by the BDFL (or a delegate) |
|||
msg266182 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2016-05-23 20:40 | |
Okay ... is anyone interested in helping with this (reviewing drafts)? Who would be the BDFL? |
|||
msg266193 - (view) | Author: Ethan Furman (ethan.furman) * | Date: 2016-05-23 21:42 | |
The BDFL would be the BDFL (Guido van Rossum ;) or whomever he asks to do the job. Don't worry too much about drafts. Pick one of the existing PEPs (409 is fairly short), and follow the same type of layouts. Make your arguments for what should be changed and why, then post it to Python-Ideas for initial discussion. As points come up either for or against it you'll add those to your draft PEP. If -Ideas doesn't kill it you can then post your most up-to-date draft to Python-Dev for the next round of discussion. Make sure to have examples of the desired behavior, and decent arguments to refute the objections already raised in this issue. |
|||
msg266199 - (view) | Author: Graham Dumpleton (grahamd) | Date: 2016-05-23 22:23 | |
If someone is going to try and do anything in the area of better proxy object support, I hope you will also look at all the work I have done on that before for wrapt (https://github.com/GrahamDumpleton/wrapt). Two related issue this has already found are http://bugs.python.org/issue19072 and http://bugs.python.org/issue19070. The wrapt package uses two separate proxy object types. A base ObjectProxy and a CallableObjectProxy because of the specific issue with how callable() works. |
|||
msg266217 - (view) | Author: Terry J. Reedy (terry.reedy) * | Date: 2016-05-24 02:28 | |
Ionel, 8 core developers (an unusually large number for a tracker issue) have posted here and the closest thing to a consensus is that this is an enhancement request. Beyond that, opinions are scattered. No one is going to commit a patch in this circumstance. So Ethan is right about writing a PEP. See PEP1 for guidelines. |
|||
msg305056 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2017-10-26 14:05 | |
Hello everyone, Is anyone still interested in fixing this bug and help with whatever PEP drafting was needed for convincing people? I would sketch up a draft but for me at least it's not clear what are the disadvantages of not fixing this, so I could use some help making a unbiased document that won't attract tons of negativity and contempt (yet again). |
|||
msg305063 - (view) | Author: Raymond Hettinger (rhettinger) * | Date: 2017-10-26 15:18 | |
Marking as closed. This can be reopened if a PEP is submitted and is favorably received. |
|||
msg305064 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2017-10-26 15:30 | |
It should be open for getting some visibility, as I need some help here - Raymond, I hope you can find a way to be hospitable here and stop with the kindergarten behavior. |
|||
msg305069 - (view) | Author: R. David Murray (r.david.murray) * | Date: 2017-10-26 16:04 | |
Ionel please give commenters the benefit of the doubt. In this case, Raymond is merely articulating our policy: if something is in pre-PEP stage we don't generally keep issues open in the tracker. We open issues for PEPs when they get to the implementation stage. So, if you want to pursue this, the best forum is the python-ideas mailing list, followed eventually by the python-dev mailing list. That is the best way to get visibility, not through a bug tracker with thousands of open issues. |
|||
msg305070 - (view) | Author: Ionel Cristian Mărieș (ionelmc) | Date: 2017-10-26 16:16 | |
Alright, now it makes sense. Thank you for writing a thoughtful response. |
History | |||
---|---|---|---|
Date | User | Action | Args |
2022-04-11 14:58:15 | admin | set | github: 68178 |
2017-10-26 16:16:31 | ionelmc | set | messages: + msg305070 |
2017-10-26 16:04:18 | r.david.murray | set | status: open -> closed messages: + msg305069 |
2017-10-26 15:30:09 | ionelmc | set | status: closed -> open messages: + msg305064 |
2017-10-26 15:18:26 | rhettinger | set | status: open -> closed messages: + msg305063 assignee: rhettinger -> versions: + Python 3.7, - Python 3.5 |
2017-10-26 14:05:19 | ionelmc | set | status: closed -> open messages: + msg305056 |
2016-05-24 02:28:45 | terry.reedy | set | messages: + msg266217 |
2016-05-23 22:23:42 | grahamd | set | messages: + msg266199 |
2016-05-23 21:42:22 | ethan.furman | set | messages: + msg266193 |
2016-05-23 20:40:08 | ionelmc | set | messages: + msg266182 |
2016-05-23 20:21:49 | ethan.furman | set | messages: + msg266181 |
2016-05-23 20:15:36 | ionelmc | set | messages: + msg266180 |
2016-05-23 20:04:35 | ethan.furman | set | messages: + msg266179 |
2016-05-23 19:49:27 | ionelmc | set | messages: + msg266177 |
2016-05-23 18:34:18 | ethan.furman | set | messages: + msg266172 |
2016-05-23 15:39:37 | ionelmc | set | files: + callable2.diff |
2016-05-23 13:46:51 | ionelmc | set | messages: + msg266149 |
2016-01-13 10:10:46 | grahamd | set | nosy:
+ grahamd |
2015-04-20 15:50:47 | eryksun | set | nosy:
+ eryksun messages: + msg241665 |
2015-04-20 15:38:24 | ionelmc | set | messages: + msg241664 |
2015-04-20 14:42:02 | rhettinger | set | messages: + msg241653 |
2015-04-20 14:08:25 | eric.snow | set | messages: + msg241651 |
2015-04-20 14:02:22 | eric.snow | set | messages: + msg241650 |
2015-04-20 13:53:06 | r.david.murray | set | messages: + msg241649 |
2015-04-20 13:45:44 | ionelmc | set | messages: + msg241648 |
2015-04-20 13:36:08 | ionelmc | set | messages: + msg241645 |
2015-04-20 13:28:21 | r.david.murray | set | messages: + msg241644 |
2015-04-20 13:17:23 | r.david.murray | set | status: open -> closed resolution: rejected messages: + msg241640 stage: resolved |
2015-04-20 08:43:13 | ionelmc | set | messages: + msg241624 |
2015-04-20 08:41:08 | ionelmc | set | messages: + msg241623 |
2015-04-20 04:02:27 | rhettinger | set | messages: + msg241597 |
2015-04-20 01:50:45 | eric.snow | set | messages: + msg241587 |
2015-04-20 01:43:08 | eric.snow | set | messages: + msg241586 |
2015-04-20 00:50:15 | ionelmc | set | messages: + msg241581 |
2015-04-20 00:41:01 | ionelmc | set | messages: + msg241580 |
2015-04-19 23:59:17 | eric.snow | set | messages: + msg241573 |
2015-04-19 22:33:20 | ionelmc | set | messages: + msg241571 |
2015-04-19 22:12:50 | eric.snow | set | messages: + msg241570 |
2015-04-19 22:10:25 | eric.snow | set | messages: + msg241569 |
2015-04-19 22:09:14 | eric.snow | set | messages: + msg241568 |
2015-04-19 21:27:41 | rhettinger | set | assignee: rhettinger nosy: + rhettinger |
2015-04-19 20:12:01 | ionelmc | set | messages: + msg241562 |
2015-04-19 19:36:33 | llllllllll | set | messages: + msg241557 |
2015-04-19 19:29:33 | steven.daprano | set | nosy:
+ steven.daprano messages: + msg241554 |
2015-04-19 19:05:24 | ionelmc | set | messages: + msg241542 |
2015-04-19 19:01:45 | ethan.furman | set | messages: + msg241540 |
2015-04-19 18:28:19 | ionelmc | set | messages: + msg241530 |
2015-04-19 18:01:27 | eric.snow | set | nosy:
+ eric.snow messages: + msg241524 |
2015-04-18 19:33:28 | r.david.murray | set | messages: + msg241452 |
2015-04-18 19:24:05 | r.david.murray | set | status: closed -> open versions: + Python 3.5, - Python 2.7, Python 3.2, Python 3.3, Python 3.4 messages: + msg241451 resolution: not a bug -> (no value) stage: resolved -> (no value) |
2015-04-18 19:21:44 | ethan.furman | set | messages: + msg241450 |
2015-04-18 18:52:19 | r.david.murray | set | status: open -> closed type: enhancement -> behavior versions: + Python 2.7, Python 3.2, Python 3.3, Python 3.4, - Python 3.5 nosy: + r.david.murray messages: + msg241440 resolution: not a bug stage: test needed -> resolved |
2015-04-18 18:00:34 | christian.heimes | set | messages: + msg241435 |
2015-04-18 17:43:12 | ionelmc | set | messages: + msg241433 |
2015-04-18 17:35:10 | christian.heimes | set | messages: + msg241431 |
2015-04-18 17:16:42 | ethan.furman | set | messages: + msg241430 |
2015-04-18 16:47:14 | ionelmc | set | messages: + msg241426 |
2015-04-18 16:44:53 | ionelmc | set | messages: + msg241425 |
2015-04-18 16:23:18 | ethan.furman | set | messages: + msg241423 |
2015-04-18 10:30:12 | ionelmc | set | messages: + msg241407 |
2015-04-18 05:31:17 | ethan.furman | set | messages: + msg241398 |
2015-04-18 04:45:18 | terry.reedy | set | messages: + msg241397 |
2015-04-18 01:28:50 | llllllllll | set | messages: + msg241389 |
2015-04-18 01:19:22 | ethan.furman | set | type: behavior -> enhancement messages: + msg241388 versions: + Python 3.5, - Python 2.7, Python 3.2, Python 3.3, Python 3.4 |
2015-04-18 00:14:57 | llllllllll | set | type: enhancement -> behavior messages: + msg241387 versions: + Python 2.7, Python 3.2, Python 3.3, Python 3.4, - Python 3.5 |
2015-04-18 00:11:10 | terry.reedy | set | versions:
+ Python 3.5, - Python 2.7, Python 3.2, Python 3.3, Python 3.4 nosy: + terry.reedy messages: + msg241386 type: behavior -> enhancement stage: test needed |
2015-04-18 00:09:27 | llllllllll | set | messages: + msg241384 |
2015-04-17 23:31:06 | ionelmc | set | messages: + msg241383 |
2015-04-17 23:28:05 | jedwards | set | nosy:
+ jedwards messages: + msg241381 |
2015-04-17 23:15:52 | ionelmc | set | messages: + msg241380 |
2015-04-17 23:13:37 | llllllllll | set | messages: + msg241379 |
2015-04-17 23:07:25 | belopolsky | set | messages: + msg241377 |
2015-04-17 23:00:44 | belopolsky | set | status: closed -> open nosy: + belopolsky messages: + msg241375 resolution: not a bug -> (no value) stage: resolved -> (no value) |
2015-04-17 22:52:59 | llllllllll | set | files: - callable.diff |
2015-04-17 22:52:42 | llllllllll | set | files:
+ callable.diff messages: + msg241373 |
2015-04-17 22:42:48 | ionelmc | set | messages: + msg241371 |
2015-04-17 20:41:58 | ionelmc | set | messages: + msg241365 |
2015-04-17 20:32:37 | ethan.furman | set | messages: + msg241364 |
2015-04-17 20:22:38 | ionelmc | set | messages: + msg241362 |
2015-04-17 20:18:33 | ethan.furman | set | status: open -> closed nosy: + ethan.furman messages: + msg241361 resolution: not a bug |
2015-04-17 20:14:45 | christian.heimes | set | messages: + msg241360 |
2015-04-17 20:01:09 | llllllllll | set | files:
+ callable.diff keywords: + patch messages: + msg241358 |
2015-04-17 19:55:56 | llllllllll | set | nosy:
+ llllllllll |
2015-04-17 19:38:16 | ionelmc | set | status: closed -> open resolution: not a bug -> (no value) messages: + msg241355 |
2015-04-17 19:30:51 | christian.heimes | set | status: open -> closed nosy: + christian.heimes messages: + msg241354 resolution: not a bug stage: resolved |
2015-04-17 19:26:37 | Claudiu.Popa | set | nosy:
+ Claudiu.Popa |
2015-04-17 19:22:38 | ionelmc | set | messages: + msg241353 |
2015-04-17 18:52:48 | ionelmc | create |