python lambda functions behavior
Question:
I am having a hard time understanding the behavior of these lambda functions in python 3.10.0
I am trying to define the NOT
logical operator from lambda calculus (see, e.g., the definition on wikipedia
The following implementation is correct:
In [1]: TRUE = lambda a: lambda b: a
...: FALSE = lambda a: lambda b: b
...: NOT = lambda a: a(FALSE)(TRUE)
...: assert NOT(FALSE) == TRUE
However, when I try and do a literal substitution either for FALSE
or TRUE
, the code fails
In [2]: NOT1 = lambda a: a(FALSE)(lambda a: lambda b: a)
...: assert NOT1(FALSE) == TRUE
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[2], line 2
1 NOT1 = lambda a: a(FALSE)(lambda a: lambda b: a)
----> 2 assert NOT1(FALSE) == TRUE
AssertionError:
Can anybody explain me why this happens?
Answers:
Python function ==
works by object identity. (Trying to implement it any other way ranges from "painfully inconsistent" to "plain impossible".) You have created another function with the same behavior as your NOT
function, but it is not the same function object, so ==
says they’re not equal.
The main principle is that every invocation of lambda
creates a new function.
Your first cell is exactly this:
def TRUE(a):
def _inner(b):
return a
return _inner
def FALSE(a):
def _inner(b):
return b
return _inner
def NOT(a):
return a(FALSE)(TRUE)
Executing NOT(FALSE)
results in FALSE(FALSE)(TRUE)
results in FALSE._inner(TRUE)
returning TRUE, a function.
Now your second cell, executing NOT1(FALSE)
results in FALSE(FALSE)(lambda...)
results in FALSE._inner(lambda...)
returning lambda...
, another function but not the same function defined in TRUE
. Remember the principle I said earlier? That lambda
statement created a new function.
The ==
operator when comparing two functions does not concern itself with the contents of the function. It only concerns itself whether the items being compared are pointing to the exact same function in memory. But since TRUE
and lambda...
are two separate functions — at different memory locations even if the contents are the same — then ==
comparison fails.
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 id
s
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'|x00Sx00'
In [26]: g.__code__.co_code
Out[26]: b'|x00Sx00'
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'|x00Sx00'
In [43]: g.__code__.co_code
Out[43]: b'|x00Sx00'
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?
I am having a hard time understanding the behavior of these lambda functions in python 3.10.0
I am trying to define the NOT
logical operator from lambda calculus (see, e.g., the definition on wikipedia
The following implementation is correct:
In [1]: TRUE = lambda a: lambda b: a
...: FALSE = lambda a: lambda b: b
...: NOT = lambda a: a(FALSE)(TRUE)
...: assert NOT(FALSE) == TRUE
However, when I try and do a literal substitution either for FALSE
or TRUE
, the code fails
In [2]: NOT1 = lambda a: a(FALSE)(lambda a: lambda b: a)
...: assert NOT1(FALSE) == TRUE
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[2], line 2
1 NOT1 = lambda a: a(FALSE)(lambda a: lambda b: a)
----> 2 assert NOT1(FALSE) == TRUE
AssertionError:
Can anybody explain me why this happens?
Python function ==
works by object identity. (Trying to implement it any other way ranges from "painfully inconsistent" to "plain impossible".) You have created another function with the same behavior as your NOT
function, but it is not the same function object, so ==
says they’re not equal.
The main principle is that every invocation of lambda
creates a new function.
Your first cell is exactly this:
def TRUE(a):
def _inner(b):
return a
return _inner
def FALSE(a):
def _inner(b):
return b
return _inner
def NOT(a):
return a(FALSE)(TRUE)
Executing NOT(FALSE)
results in FALSE(FALSE)(TRUE)
results in FALSE._inner(TRUE)
returning TRUE, a function.
Now your second cell, executing NOT1(FALSE)
results in FALSE(FALSE)(lambda...)
results in FALSE._inner(lambda...)
returning lambda...
, another function but not the same function defined in TRUE
. Remember the principle I said earlier? That lambda
statement created a new function.
The ==
operator when comparing two functions does not concern itself with the contents of the function. It only concerns itself whether the items being compared are pointing to the exact same function in memory. But since TRUE
and lambda...
are two separate functions — at different memory locations even if the contents are the same — then ==
comparison fails.
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 id
s
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'|x00Sx00'
In [26]: g.__code__.co_code
Out[26]: b'|x00Sx00'
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'|x00Sx00'
In [43]: g.__code__.co_code
Out[43]: b'|x00Sx00'
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?