Redefine Method of an Object
Asked Answered
D

2

5

I've got a class, where a method should only run once. Of course, it could easily be done with artificial has_executed = True/False flag, but why use it, if you can just delete the method itself? python's a duck-typed language, everything is a reference, bla-bla-bla, what can go wrong?

At least it was the thought. I couldn't actually do it:

class A:    
    def b(self):
        print("empty")
        self.__delattr__('b')

a = A()
a.b()

raises AttributeError: b. However, executing self.__getattribute__('b') returns <bound method A.b of <__main__.A object at 0x000001CDC6742FD0>>, which sounds stupid to me: why is a method any different from an attribute, since everything in python is just a reference to an object? And why can I __getattribute__, but not __delattr__?

The same goes to redefinition. I can easily set any attribute, but methods are a no-no?

class A:
    def b(self):
        print("first")
        self.__setattr__('b', lambda self: print(f"second"))

a = A()
a.b()
a.b()

results into TypeError: <lambda>() missing 1 required positional argument: 'self'. Which, of course, means, that now python isn't using dot-notation as intended. Of course, we could ditch the self attribute in the lambda altogether, considering we've got the reference to it already in b. But isn't it incorrect by design?

The further I'm trying to take python to the limit, the more frustrated I become. Some imposed limitations (or seemingly imposed?) seem so unnatural, considering the way the language is marketed. Shouldn't it allow this? Why doesn't it work?

UPD

Ok, consider this:

class A:
    def __init__(self):
        self.variable = 1

    def b(self):
        print("old")
        self.variable += 1
        def new_b():
            print("new")
            self.variable += 15
        self.__setattr__('b', new_b)

It will work and do what we want: none of other objects will have their A.b method redefined once one object kind of overlays its b definition. (overlays, since everyone so far says that you cannot redefine a method for an object, but instead only kind of hide it from the caller behind another attribute with the same name, as far as I understand).

Is this good?

Deltadeltaic answered 18/10, 2021 at 14:43 Comment(0)
B
6

It doesn't work because b isn't an attribute belonging to the instance, it belongs to the class. So you can't delete it on the instance because it isn't there to be deleted.

>>> a = A()
>>> list(a.__dict__)
[]
>>> list(A.__dict__)
['__module__', 'b', '__dict__', '__weakref__', '__doc__']

When a.b is evaluated, Python will see that a has no instance attribute named b and fall back to the class. (It's a little more complicated because when falling back to the class, it will not simply return the method itself, but a version of the method which is bound to the instance a.)

Since you don't want to delete the method on the class, the way to go is to replace the method on the instance. I don't know why you tried to do this with __setattr__ - there is no need for that, simply assign self.b = ... as normal. The reason your attempt failed is because your lambda requires a positional parameter named self, but this parameter will not be automatically bound to the instance when you look it up, because it is an instance attribute, not a class attribute.

class A:
    def b(self):
        print('first')
        self.b = lambda: print('second')

Usage:

>>> a = A()
>>> a.b()
first
>>> a.b()
second
Balaam answered 18/10, 2021 at 14:52 Comment(2)
Code-wise, I wrote exactly that, just with __setattr__ instead of self.b = .... But what if you actually want to use self in the redefined method? Do you just casually reference it from the old b's scope? I updated the question.Deltadeltaic
You wrote it where the lambda has a parameter, which means you need to pass an argument when you call it. Since you want to call it with no arguments, you need the lambda to have no parameters. Yes, you can just refer to self from the outer scope, which references the correct object; alternatively if you don't like closures, you could declare a method second_b on the class, and then write self.b = self.second_b in the method (this will be bound to the instance because self.second_b is a class lookup). The disadvantage of that is that you would be able to call a.second_b() any time.Balaam
H
1

Well in python you have 2 types of attributes

  • A class attribute is a variable that belongs to a certain class, and not a particular object. Every instance of this class shares the same variable. These attributes are usually defined outside the init constructor
  • An instance/object attribute is a variable that belongs to one (and only one) object. Every instance of a class points to its own attributes variables. These attributes are defined within the init constructor.

In case of a class attribute its part of the class descriptor, so you cannot delete it from the object attributes like self.__deleteattr__ or add new one with __setattr__ as it alters the class descriptor and reflects on all objects. Such an operation can have devastating effects.

Its very similar to a class variable as well. You can however change the behavior with overriding or reassigning like below

class A:    
    def b(self):
        print("empty")
        A.b =  lambda self: print(f"second")

a = A()
a.b()
a.b()
Hawkeyed answered 18/10, 2021 at 14:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.