22

I would like to be able to test whether two callable objects are the same or not. I would prefer identity semantics (using the "is" operator), but I've discovered that when methods are involved, something different happens.

#(1) identity and equality with a method
class Foo(object):
    def bar(self):
        pass
foo = Foo()
b = foo.bar
b == foo.bar    #evaluates True. why?
b is foo.bar    #evaluates False. why?

I've reproduced this with both Python 2.7 and 3.3 (CPython) to make sure it's not an implementation detail of the older version. In other cases, identity testing works as expected (interpreter session continued from above):

#(2) with a non-method function
def fun(self):
    pass
f = fun
f == fun    #evaluates True
f is fun    #evaluates True

#(3) when fun is bound as a method 
Foo.met = fun
foo.met == fun    #evaluates False
foo.met is fun    #evaluates False

#(4) with a callable data member
class CanCall(object):
    def __call__(self):
        pass
Foo.can = CanCall()
c = foo.can
c == foo.can    #evaluates True
c is foo.can    #evaluates True

According to the question How does Python distinguish callback function which is a member of a class?, a function is wrapped when bound as a method. This makes sense and is consistent with case (3) above.

Is there a reliable way to bind a method to some other name and then later have them compare equal like a callable object or a plain function would? If the "==" does the trick, how does that work? Why do "==" and "is" behave differently in case (1) above?

Edit

As @Claudiu pointed out, the answer to Why don't methods have reference equality? is also the answer to this question.

4

4 回答 4

16

Python 不会foo.bar为每个fooclass实例保留一个规范对象Foo。相反,当 Python 求值时会创建一个方法对象foo.bar。因此,

foo.bar is not foo.bar

至于==,事情变得一团糟。Python 3.8修复了方法比较,因此如果两个方法表示同一对象的相同方法,则它们是相等的,但在较低版本上,行为不一致。

Python 具有数量惊人的方法对象类型,具体取决于该方法是用 Python 实现还是用 C 实现方法的几种方法之一。在 Python 3.8 之前,这些方法对象类型的响应==不同:

  • 对于用 Python 编写的方法,==比较方法__func____self__属性,如果方法对象表示由相同函数实现并绑定到相等对象而不是相同对象的方法,则返回 True。因此,x.foo == y.foo如果x == y并且foo是用 Python 编写的,则为 True。
  • 对于大多数“特殊”方法(__eq____repr__等),如果它们是用 C 实现的,Python 会比较__self__和类似的内部事物__func__,如果方法具有相同的实现并且绑定到相等的对象,则再次返回 True 。
  • 对于在 C 中实现的其他方法,Python 会执行您实际期望的操作,如果方法对象表示同一对象的相同方法,则返回 True。

因此,如果您在低于 3.8 的 Python 版本上运行以下代码:

class Foo(object):
    def __eq__(self, other):
        return True if isinstance(other, Foo) else NotImplemented
    def foo(self):
        pass

print(Foo().foo == Foo().foo)
print([].__repr__ == [].__repr__)
print([].append == [].append)

你得到以下奇怪的输出

True
True
False

要在较低版本上获得 Python 3.8 语义,您可以使用

meth1.__self__ is meth2.__self__ and meth1 == meth2
于 2013-08-13T18:59:12.340 回答
1

tldr:方法是描述符,这就是为什么会发生这种情况。==如果您确实需要比较是否相等,请使用。

is(实际上)测试 的相等性id。所以让我们检查一下:

>>> id(foo.bar)
4294145364L
>>> id(foo.bar)
4294145364L
>>> id(foo.bar)
4294145364L
>>> b = foo.bar
>>> id(foo.bar)
4293744796L
>>> id(foo.bar)
4293744796L
>>> b()
>>> id(foo.bar)
4293744796L
>>> b = 1
>>> id(foo.bar)
4294145364L
>>> type(foo.bar)
<type 'instancemethod'>
>>>

因此,直接原因是表达式foo.bar间歇性地返回不同的对象。

如果您确实需要检查是否相等,只需使用==. 但是,我们都想弄清楚这一点。

>>> foo.__dict__['bar']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'bar'
>>> Foo.__dict__['bar']
<function bar at 0xffe2233c>
>>> getattr(foo, 'bar')
<bound method Foo.bar of <__main__.Foo object at 0xffe2f9ac>>
>>> foo.bar
<bound method Foo.bar of <__main__.Foo object at 0xffe2f9ac>>
>>>

看起来绑定方法有一些特别之处。

>>> type(foo.bar)
<type 'instancemethod'>
>>> help(type(foo.bar))
Help on class instancemethod in module __builtin__:

class instancemethod(object)
 |  instancemethod(function, instance, class)
 |
 |  Create an instance method object.
 |
 |  Methods defined here:
 |
 |  __call__(...)
 |      x.__call__(...) <==> x(...)
 |
 |  __cmp__(...)
 |      x.__cmp__(y) <==> cmp(x,y)
 |
 |  __delattr__(...)
 |      x.__delattr__('name') <==> del x.name
 |
 |  __get__(...)
 |      descr.__get__(obj[, type]) -> value
 |
 |  __getattribute__(...)
 |      x.__getattribute__('name') <==> x.name
 |
 |  __hash__(...)
 |      x.__hash__() <==> hash(x)
 |
 |  __repr__(...)
 |      x.__repr__() <==> repr(x)
 |
 |  __setattr__(...)
 |      x.__setattr__('name', value) <==> x.name = value
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __func__
 |      the function (or other callable) implementing a method
 |
 |  __self__
 |      the instance to which a method is bound; None for unbound methods
 |
 |  im_class
 |      the class associated with a method
 |
 |  im_func
 |      the function (or other callable) implementing a method
 |
 |  im_self
 |      the instance to which a method is bound; None for unbound methods
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __new__ = <built-in method __new__ of type object>
 |      T.__new__(S, ...) -> a new object with type S, a subtype of T

现在,请注意这列出了一个__get__方法。这意味着该instancemethod对象是一个描述符。根据http://docs.python.org/2/reference/datamodel.html#implementing-descriptors表达式foo.bar返回(getattr(foo,'bar').__get__(foo). 这就是为什么这个值可以改变。

至于它为什么改变,我不能告诉你,除了它可能是一个实现细节。

于 2013-08-13T19:09:47.027 回答
0

您可以使用foo is barid(foo) == id(bar)来检查身份。如果要检查“平等”(值),请使用==.

于 2013-08-13T23:18:04.430 回答
0

虽然我没有你所有问题的答案,但我怀疑诀窍是__func__用于拥有它的可调用对象(即用于方法):

In [32]: def same_func(func1, func2):
   ....:     if hasattr(func1, '__func__'):
   ....:         func1 = func1.__func__
   ....:     if hasattr(func2, '__func__'):
   ....:         func2 = func2.__func__
   ....:     return func1 is func2
   ....: 

In [33]: same_func(b, foo.bar)
Out[33]: True

In [34]: same_func(f, fun)
Out[34]: True

In [35]: same_func(foo.met, fun)
Out[35]: True

In [36]: same_func(c, foo.can)
Out[36]: True
于 2013-08-13T18:52:44.467 回答