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.

Author ionelmc
Recipients Claudiu.Popa, belopolsky, christian.heimes, eric.snow, ethan.furman, ionelmc, jedwards, llllllllll, r.david.murray, steven.daprano, terry.reedy
Date 2015-04-19.20:12:01
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <CANkHFr_-YkAeM6gYqAn5ToNTibNHctTxVLdc+OcOMFvoBKYtnA@mail.gmail.com>
In-reply-to <1429471773.69.0.137724983311.issue23990@psf.upfronthosting.co.za>
Content
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
History
Date User Action Args
2015-04-19 20:12:01ionelmcsetrecipients: + ionelmc, terry.reedy, belopolsky, christian.heimes, steven.daprano, r.david.murray, Claudiu.Popa, ethan.furman, eric.snow, llllllllll, jedwards
2015-04-19 20:12:01ionelmclinkissue23990 messages
2015-04-19 20:12:01ionelmccreate