Thanks to all who clarified the issue with the == operator.
I have tried to understand where exactly the two functions are different.
Let's start with the simpler example
In [18]: id(f)
Out[18]: 140465987909328
In [19]: id(g)
Out[19]: 140465990159088
In [20]: assert f==g
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[20], line 1
----> 1 assert f==g
AssertionError:
clearly the two objects have different ids
In [21]: id(f)
Out[21]: 140465987909328
In [22]: id(g)
Out[22]: 140465990159088
what is less obvious to me is that they do have different hash values
In [23]: f.__hash__()
Out[23]: 8779124244333
In [24]: g.__hash__()
Out[24]: 8779124384943
The equivalence test that I need can be done on the __code__.co_code function attribute.
In [25]: f.__code__.co_code
Out[25]: b'|\x00S\x00'
In [26]: g.__code__.co_code
Out[26]: b'|\x00S\x00'
In [27]: assert f.__code__.co_code == g.__code__.co_code
I was curious, however, where exactly the two function objectss differ.
Looking at the methods and attributes of __code__, we have
In [28]: dir(f.__code__)
Out[28]:
['__class__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'co_argcount',
'co_cellvars',
'co_code',
'co_consts',
'co_filename',
'co_firstlineno',
'co_flags',
'co_freevars',
'co_kwonlyargcount',
'co_lines',
'co_linetable',
'co_lnotab',
'co_name',
'co_names',
'co_nlocals',
'co_posonlyargcount',
'co_stacksize',
'co_varnames',
'replace']
So we try and verify where
In [29]: L = [x for x in dir(f.__code__) if x.startswith('co_')]
In [29]: for x in L:
...: print(f"{x:20s}", getattr(f.__code__,x)==getattr(g.__code__,x))
...:
co_argcount True
co_cellvars True
co_code True
co_consts True
co_filename False
co_firstlineno True
co_flags True
co_freevars True
co_kwonlyargcount True
co_lines False
co_linetable True
co_lnotab True
co_name True
co_names True
co_nlocals True
co_posonlyargcount True
co_stacksize True
co_varnames True
The only fields that are actually different are co_filename and co_lines():
In [30]: f.__code__.co_filename, g.__code__.co_filename
Out[30]: ('<ipython-input-1-242f7af8e2bb>', '<ipython-input-2-a597939a9a2e>')
In [31]: f.__code__.co_lines()
Out[31]: <line_iterator at 0x7f10948790c0>
In [32]: g.__code__.co_lines()
Out[32]: <line_iterator at 0x7f10945178c0>
In [33]: list(f.__code__.co_lines())
Out[33]: [(0, 4, 1)]
In [34]: list(g.__code__.co_lines())
Out[34]: [(0, 4, 1)]
The co_filename field cannot be changed:
In [35]: f.__code__.co_filename = 'something_else'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[35], line 1
----> 1 f.__code__.co_filename = 'something_else'
AttributeError: readonly attribute
It seems to me that if the only things that are actually different are these two fields.
Interestingly enough, there is a case for which two lambda functions with different variable names should also be considered the same:
In [40]: f = lambda x:x
In [41]: g = lambda y:y
In [42]: f.__code__.co_code
Out[42]: b'|\x00S\x00'
In [43]: g.__code__.co_code
Out[43]: b'|\x00S\x00'
and in fact they are, as far as the co_code is concerned.
In conclusion, I think that there could be an argument for introducing an equivalence operator for functions, different than the __eq__ operator, maybe something with a syntax ~=, which does not appear to have been taken yet. Your thoughts?
a = lambda x: x; b = lambda x: x; assert a == balso fails. I imagine that this is because of how__eq__is implemented for python functions, although I do not know the details.