Proposal: change the default behavior of lambda function of class

I started a issue in: report very strange behavior of user-defined class lambda function · Issue #126666 · python/cpython · GitHub

But I still insist that the behavior is not acceptable, I make another mini example:

class B:
    def __call__(self, x):
        return x

fn_b = B()
fn_c = lambda x: x

class X:
    fn_b = fn_b
    fn_c = fn_c

assert fn_b(1) == fn_c(1)
assert X.fn_b(1) == X.fn_c(1)
assert X().fn_b(1) == X().fn_c(1) # ERROR: TypeError: <lambda>() takes 1 positional argument but 2 were given

# true behavior:
# x = X()
# assert x.fn_b() == x

When we define a variable and assign the variable a value, may be any value, I think We should never treat the variable as a class-method MERELY because the variable is a function!

in the above case, fn_c could be imported from other module.

AND:
X().fn_b(1) is OK
X().fn_c(1) is an ERROR, strange and unacceptable, NOT symmetric

when we x.fn_b, we are just searching a attribute named fn_b, if attribute not found, we would continue our searching from the class of the instance. It is straightforward.

looking forward to your opinions!
Many Thanks!

Why? What should define it as a class method?

We can define class-method like:

class A:
      cls_method1 = classmethod(lambda cls, x: x)

      @classmethod
      def cls_method2(cls, x):
            return x

we can define classmethod in a more explicitly way, just as @skirpichev mentioned
in report very strange behavior of user-defined class lambda function · Issue #126666 · python/cpython · GitHub

I think you slightly misinterpret my example. Consider a more long one:

>>> class A:
...     f = lambda x: print(x)
...     @classmethod
...     def g(cls):
...         print(cls)
...         
>>> A().f()
<__main__.A object at 0x7f489afa3340>
>>> A().g()
<class '__main__.A'>
  • there is a difference, when you call f() and g() on an instance.

It’s important to remember that lambdas are just functions.

>>> import types
>>> types.LambdaType
<class 'function'>

Expecting any asymmetries between how lambdas and functions behave beyond how they’re defined is a much deeper change than you might think.

3 Likes

The behaviour in your example is the behaviour I would expect.

Functions (and hence lambdas) have a __get__ method which changes how they are accessed on instances and is what makes methods work.

Your callable B class does not define this method, so it’s accessed as a normal attribute and not a descriptor.

3 Likes

Dear gseismic,

what you have observed is neither strange nor an error, it is how Python works since more than 30 years now:

>>> class Ham:
...     def spam(x): print(x)
... 
>>> h = Ham()
>>> h.spam() # OK
>>> h.spam(0) # Error

It is the same if you define the function outside and assign:

>>> def func(x): print(x)
>>> class Ham:
...     spam = func

It is also the same for Lambdas (which are functions, as Brénainn Woodsend already poined out), which are functions as well:

>>> func = lambda x: print x
>>> class Ham:
...     spam = lambda x: print x
...     spam2 = func

Its more like that other callable types (like your class B) are the odd ones.

Its not that straightforward for any programming language that has member functions; consider for example C++:

struct Ham {
     void spam() { std::cout << this; }
};
int main() {
     Ham h;
     h.spam(); 
}

Here, h.spam also binds the member function spam to the object h. With C++23, this can be done even in quite Pythonic way:

struct Ham {
     void spam(this Ham &self) { std::cout << &self; }
};
int main() {
     Ham h;
     h.spam();  // OK
     h.spam(h); // error
}
2 Likes

actually:

what I mean:

for:

class A:
        f = lambda x: x  

a = A()
a.f() → trigger bind, actually do: [scope-of(a), that is A]::f(a)
eq to:

def f(self): 
    return self; 

what I formerly expected:
a.f → do not trigger bind, do [scope-of(a), that is A]::f,
I am expecting f = lambda x: x equivalent to:

class A:
        f = staticmethod(lambda x: x)

Thank you all!

The example I made was not good, here is another one:

def test2():
    class B:
        pass

    class A:
        pass

    B.t1 = staticmethod(lambda x: x)
    A.t1 = B.t1

    assert B.t1(1) == 1
    assert A.t1(1) == 1
    assert B().t1(1) == 1
    assert A().t1(1) == 1 # ERROR

I think it is a little strange, although I can solve like:
A.t1 = B.dict[‘t1’] OK

A static method returns the original function when retrieved via a class or instance attribute and that original function has no awareness that it was being used as a static method before. The correct way to do this would be A.t1 = staticmethod(B.t1).

2 Likes

Again, this behaviour is expected.

To demonstrate what’s going on, first make the lambda and static/class method versions at module level and then assign all of them to the class.

class A: pass

a_inst = A()

lamb = lambda x: x
slamb = staticmethod(lamb)
clamb = classmethod(lamb)

A.lamb = lamb
A.slamb = slamb
A.clamb = clamb

# This __get__ call is what happens when you access class attributes
assert A.lamb == lamb.__get__(None, A)
assert A.slamb == slamb.__get__(None, A)
assert A.clamb == clamb.__get__(None, A)

# And this __get__ call for instance attributes
assert a_inst.lamb == lamb.__get__(a_inst, A)
assert a_inst.slamb == slamb.__get__(a_inst, A)
assert a_inst.clamb == clamb.__get__(a_inst, A)

So the value you retrieve from the class or instance is not necessarily the original object you assigned if the class of the object has a __get__ method. This is useful for a number of things, for example the descriptor guide contains a demonstration of this to implement a pure Python equivalent to staticmethod.

1 Like

As I perceive it, the interesting part is that a callable object will not be treated the same way as a real function object - they will be left unbounded.

I do not think this is a bug though.

If there is anything missing, I would say it is a membermethod decorator which bounds a callable object as if it is a real function object:

class Hello:
    @staticmethod
    def __call__(item = "(no argument)"):
        print(f"Hello, {item}")

hello = Hello()

class X:
    hello1 = staticmethod(hello)
    hello2 = classmethod(hello)
    hello3 = membermethod(hello) # Currently not exist


X.hello1()   # Hello, (no argument)
X.hello2()   # Hello, <class 'X'>
X().hello3() # Hello, <X object at 0x102d59550>

Not sure if anyone would ever need it :rofl:

Actually, membermethod can be done in pure python like this:

from functools import wraps

def membermethod(obj: object):
    @wraps(obj)
    def fn(*args, **kwargs):
        return obj(*args, **kwargs)
    return fn

If you’re making a callable class, you can define a __get__ method with the appropriate behaviour on the class itself.

import types

class Hello:
    @staticmethod
    def __call__(item="(no argument)"):
        print(f"Hello, {item}")

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return types.MethodType(self, obj)
1 Like

This is definitely the most appropriate solution.

What I drafted there assumes you cannot modify the implementation of a callable class. But as I said, no one will really want to do that.

You can write a wrapper in much the same way.

import types

class MemberMethod:
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.f
        return types.MethodType(self.f, obj)
1 Like

Thanks, I learnt a new trick from you :rofl: