Как следует проверять функции для равенства или идентичности?
Я хотел бы иметь возможность проверить, являются ли два вызываемых объекта одинаковыми или нет. Я бы предпочел идентификационную семантику (используя оператор "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)
для проверки личности. Если вы хотите проверить "равенство" (значение), используйте ==
.