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.

classification
Title: Callable builtin doesn't respect descriptors
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.7
process
Status: closed Resolution: rejected
Dependencies: Superseder:
Assigned To: Nosy List: Claudiu.Popa, belopolsky, christian.heimes, eric.snow, eryksun, ethan.furman, grahamd, ionelmc, jedwards, llllllllll, r.david.murray, rhettinger, steven.daprano, terry.reedy
Priority: normal Keywords: patch

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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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 y​our 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) * (Python committer) Date: 2015-04-18 17:16
I am happy to be proven wrong.  :)
msg241431 - (view) Author: Christian Heimes (christian.heimes) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) Date: 2015-04-20 13:53
Because special methods are special.
msg241650 - (view) Author: Eric Snow (eric.snow) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python triager) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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:15adminsetgithub: 68178
2017-10-26 16:16:31ionelmcsetmessages: + msg305070
2017-10-26 16:04:18r.david.murraysetstatus: open -> closed

messages: + msg305069
2017-10-26 15:30:09ionelmcsetstatus: closed -> open

messages: + msg305064
2017-10-26 15:18:26rhettingersetstatus: open -> closed

messages: + msg305063
assignee: rhettinger ->
versions: + Python 3.7, - Python 3.5
2017-10-26 14:05:19ionelmcsetstatus: closed -> open

messages: + msg305056
2016-05-24 02:28:45terry.reedysetmessages: + msg266217
2016-05-23 22:23:42grahamdsetmessages: + msg266199
2016-05-23 21:42:22ethan.furmansetmessages: + msg266193
2016-05-23 20:40:08ionelmcsetmessages: + msg266182
2016-05-23 20:21:49ethan.furmansetmessages: + msg266181
2016-05-23 20:15:36ionelmcsetmessages: + msg266180
2016-05-23 20:04:35ethan.furmansetmessages: + msg266179
2016-05-23 19:49:27ionelmcsetmessages: + msg266177
2016-05-23 18:34:18ethan.furmansetmessages: + msg266172
2016-05-23 15:39:37ionelmcsetfiles: + callable2.diff
2016-05-23 13:46:51ionelmcsetmessages: + msg266149
2016-01-13 10:10:46grahamdsetnosy: + grahamd
2015-04-20 15:50:47eryksunsetnosy: + eryksun
messages: + msg241665
2015-04-20 15:38:24ionelmcsetmessages: + msg241664
2015-04-20 14:42:02rhettingersetmessages: + msg241653
2015-04-20 14:08:25eric.snowsetmessages: + msg241651
2015-04-20 14:02:22eric.snowsetmessages: + msg241650
2015-04-20 13:53:06r.david.murraysetmessages: + msg241649
2015-04-20 13:45:44ionelmcsetmessages: + msg241648
2015-04-20 13:36:08ionelmcsetmessages: + msg241645
2015-04-20 13:28:21r.david.murraysetmessages: + msg241644
2015-04-20 13:17:23r.david.murraysetstatus: open -> closed
resolution: rejected
messages: + msg241640

stage: resolved
2015-04-20 08:43:13ionelmcsetmessages: + msg241624
2015-04-20 08:41:08ionelmcsetmessages: + msg241623
2015-04-20 04:02:27rhettingersetmessages: + msg241597
2015-04-20 01:50:45eric.snowsetmessages: + msg241587
2015-04-20 01:43:08eric.snowsetmessages: + msg241586
2015-04-20 00:50:15ionelmcsetmessages: + msg241581
2015-04-20 00:41:01ionelmcsetmessages: + msg241580
2015-04-19 23:59:17eric.snowsetmessages: + msg241573
2015-04-19 22:33:20ionelmcsetmessages: + msg241571
2015-04-19 22:12:50eric.snowsetmessages: + msg241570
2015-04-19 22:10:25eric.snowsetmessages: + msg241569
2015-04-19 22:09:14eric.snowsetmessages: + msg241568
2015-04-19 21:27:41rhettingersetassignee: rhettinger

nosy: + rhettinger
2015-04-19 20:12:01ionelmcsetmessages: + msg241562
2015-04-19 19:36:33llllllllllsetmessages: + msg241557
2015-04-19 19:29:33steven.dapranosetnosy: + steven.daprano
messages: + msg241554
2015-04-19 19:05:24ionelmcsetmessages: + msg241542
2015-04-19 19:01:45ethan.furmansetmessages: + msg241540
2015-04-19 18:28:19ionelmcsetmessages: + msg241530
2015-04-19 18:01:27eric.snowsetnosy: + eric.snow
messages: + msg241524
2015-04-18 19:33:28r.david.murraysetmessages: + msg241452
2015-04-18 19:24:05r.david.murraysetstatus: 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:44ethan.furmansetmessages: + msg241450
2015-04-18 18:52:19r.david.murraysetstatus: 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:34christian.heimessetmessages: + msg241435
2015-04-18 17:43:12ionelmcsetmessages: + msg241433
2015-04-18 17:35:10christian.heimessetmessages: + msg241431
2015-04-18 17:16:42ethan.furmansetmessages: + msg241430
2015-04-18 16:47:14ionelmcsetmessages: + msg241426
2015-04-18 16:44:53ionelmcsetmessages: + msg241425
2015-04-18 16:23:18ethan.furmansetmessages: + msg241423
2015-04-18 10:30:12ionelmcsetmessages: + msg241407
2015-04-18 05:31:17ethan.furmansetmessages: + msg241398
2015-04-18 04:45:18terry.reedysetmessages: + msg241397
2015-04-18 01:28:50llllllllllsetmessages: + msg241389
2015-04-18 01:19:22ethan.furmansettype: 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:57llllllllllsettype: 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:10terry.reedysetversions: + 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:27llllllllllsetmessages: + msg241384
2015-04-17 23:31:06ionelmcsetmessages: + msg241383
2015-04-17 23:28:05jedwardssetnosy: + jedwards
messages: + msg241381
2015-04-17 23:15:52ionelmcsetmessages: + msg241380
2015-04-17 23:13:37llllllllllsetmessages: + msg241379
2015-04-17 23:07:25belopolskysetmessages: + msg241377
2015-04-17 23:00:44belopolskysetstatus: closed -> open

nosy: + belopolsky
messages: + msg241375

resolution: not a bug -> (no value)
stage: resolved -> (no value)
2015-04-17 22:52:59llllllllllsetfiles: - callable.diff
2015-04-17 22:52:42llllllllllsetfiles: + callable.diff

messages: + msg241373
2015-04-17 22:42:48ionelmcsetmessages: + msg241371
2015-04-17 20:41:58ionelmcsetmessages: + msg241365
2015-04-17 20:32:37ethan.furmansetmessages: + msg241364
2015-04-17 20:22:38ionelmcsetmessages: + msg241362
2015-04-17 20:18:33ethan.furmansetstatus: open -> closed

nosy: + ethan.furman
messages: + msg241361

resolution: not a bug
2015-04-17 20:14:45christian.heimessetmessages: + msg241360
2015-04-17 20:01:09llllllllllsetfiles: + callable.diff
keywords: + patch
messages: + msg241358
2015-04-17 19:55:56llllllllllsetnosy: + llllllllll
2015-04-17 19:38:16ionelmcsetstatus: closed -> open
resolution: not a bug -> (no value)
messages: + msg241355
2015-04-17 19:30:51christian.heimessetstatus: open -> closed

nosy: + christian.heimes
messages: + msg241354

resolution: not a bug
stage: resolved
2015-04-17 19:26:37Claudiu.Popasetnosy: + Claudiu.Popa
2015-04-17 19:22:38ionelmcsetmessages: + msg241353
2015-04-17 18:52:48ionelmccreate