Как следует проверять функции для равенства или идентичности?

Я хотел бы иметь возможность проверить, являются ли два вызываемых объекта одинаковыми или нет. Я бы предпочел идентификационную семантику (используя оператор "is" ), но я обнаружил, что когда задействованы методы, происходит что-то другое.

#(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?

Я воспроизвел это как с Python 2.7, так и с 3.3 (CPython), чтобы убедиться, что это не деталь реализации старой версии. В других случаях тестирование идентичности работает как ожидалось (сеанс интерпретатора продолжался сверху):

#(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

В соответствии с вопросом Как Python отличает функцию обратного вызова, которая является членом класса?, функция обертывается при привязке как метод. Это имеет смысл и согласуется с рассмотренным выше случаем (3).

Есть ли надежный способ привязать метод к другому имени, а затем сравнить его как объект, подлежащий вызову, или обычную функцию? Если "==" делает трюк, как это работает? Почему "==" и "есть" ведут себя по-другому в случае (1) выше?

Edit

Как отметил @Claudiu, ответ на Почему методы не имеют ссылочного равенства? также является ответом на этот вопрос.

Ответы

Ответ 1

Python не сохраняет канонический foo.bar объект для каждого экземпляра foo класса foo. Вместо этого объект метода создается, когда Python оценивает foo.bar. Таким образом,

foo.bar is not foo.bar

Что касается ==, все становится беспорядочным. Python имеет удивительно большое количество типов объектов метода, в зависимости от того, был ли метод реализован в Python или одним из нескольких способов, которые методы могут быть реализованы в C. Эти типы объектов метода реагируют на == по-разному:

  • Для методов, написанных на Python, == сравнивает атрибуты методов __func__ и __self__, возвращая True, если метод объекты представляют методы, реализованные одной и той же функцией и привязанные к равным объектам, а не к одному и тому же объекту. Таким образом, x.foo == y.foo будет True, если x == y и foo написано на Python.
  • Для наиболее "специальные" методы (__eq__, __repr__ и т.д.), если они реализованы в C, Python сравнивает __self__ и внутреннюю вещь, аналогичную __func__, снова возвращает True, если методы имеют одну и ту же реализацию и привязаны к равным объектам.
  • Для других методов, реализованных в C, Python делает то, что вы на самом деле ожидаете, вернув True, если объекты метода представляют один и тот же метод тот же объект.

Таким образом, если вы запустите следующий код:

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

Вы, скорее всего, не хотите семантику идентификации, поскольку многие объекты могут представлять один и тот же метод. Вы также не должны полагаться на простые методы == для методов, поскольку семантика настолько беспорядочна, обычно бесполезна и подвержена изменениям, если метод переписывается в C. К счастью, все типы объектов метода, с которыми вы, вероятно, имеете дело, имеют a __self__ атрибут, представляющий объект, к которому они привязаны, поэтому

meth1.__self__ is meth2.__self__ and meth1 == meth2

должен быть общим способом сравнения, если два объекта метода представляют один и тот же метод одного и того же объекта.

Ответ 2

TL;DR: Методы - это дескрипторы, поэтому это может произойти. Используйте ==, если вам действительно нужно сравнить для равенства.

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). И именно поэтому это значение может измениться.

Относительно того, почему это происходит, я не могу сказать вам, кроме того, что это, скорее всего, деталь реализации.

Ответ 3

Пока у меня нет ответов на все ваши вопросы, я подозреваю, что трюк состоит в том, чтобы использовать __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

Ответ 4

Вы можете использовать foo is bar, который является тем же самым id(foo) == id(bar) для проверки личности. Если вы хотите проверить "равенство" (значение), используйте ==.