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?

Asked By: acortis

||

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.

Answered By: user2357112

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.

Answered By: pepoluan

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'|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?

Answered By: acortis
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.