1

Is there any way in __setattr__() to differentiate between an attribute set from inside the class or a child/inheriting class, and an attribute set from outside the current or a child class?

I want to change how setting attributes works from the "outside", in my case of making a module, I want the user to have different logic when setting a attribute than when it's set from inside the class.

For example:
i.x = 5 should assign 5 normally when called from within the class and i is a instance of it, but when called from another class it should, say, subtract 5 instead of set to 5.

martineau
  • 119,623
  • 25
  • 170
  • 301
laundmo
  • 318
  • 5
  • 16

3 Answers3

4

A bit lowlevel, but you could use inspect module:

import inspect

class A:

    def __init__(self):
        self.__x = 0

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, value):
        f = inspect.currentframe()
        if 'self' in f.f_back.f_locals and issubclass(type(f.f_back.f_locals['self']), A):
            print('Called from class!')
            self.__x = -value
        else:
            print('Called from outside!')
            self.__x = value

    def fn(self):
        print('Calling A.x from inside:')
        self.x = 10

class B(A):
    def __init__(self):
        super().__init__()

    def fn2(self):
        print('Calling B.x from inside:')
        self.x = 15

a = A()
print("A.x after init:", a.x)
print('Calling A.x from outside')
a.x = 10
print("A.x called from the outside:", a.x)
a.fn()
print("A.x called from the inside:", a.x)

b = B()
print("B.x after init:", b.x)
print('Calling B.x from outside')
b.x = 20
print("B.x called from the outside:", b.x)
b.fn2()
print("B.x called from the inside:", b.x)

Prints:

A.x after init: 0
Calling A.x from outside
Called from outside!
A.x called from the outside: 10
Calling A.x from inside:
Called from class!
A.x called from the inside: -10
B.x after init: 0
Calling B.x from outside
Called from outside!
B.x called from the outside: 20
Calling B.x from inside:
Called from class!
B.x called from the inside: -15
laundmo
  • 318
  • 5
  • 16
Andrej Kesely
  • 168,389
  • 15
  • 48
  • 91
3

Use a property. Inside the class, you can assign directly to the underlying attribute. Outside, assignments to x decrement it instead.

class Foo:
    def __init__(self):
         self._x = 0

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x -= value
chepner
  • 497,756
  • 71
  • 530
  • 681
  • while this technically is a working solution, another solution does the same without having to use _x inside the class. – laundmo Jun 11 '19 at 20:17
  • I consider separating the externally visible name from the underlying attribute a feature, not a drawback. – chepner Jun 11 '19 at 20:22
  • this is exactly what i wanted to accomplish WITHOUT having to change functions in subclasses where that attribute is used. The accepted answer does exactly what i was asking for by having a underlying separation without having to actually use different attribute names. – laundmo Jun 11 '19 at 20:42
  • @laundmo: Properties are inherited, so subclasses can use plain `x` to do implicit assignments to `_x` just like in the base class. The only place that needs to use `_x` is the `@x.setter` function in the base class. – martineau Jun 11 '19 at 21:28
  • @martineau the thing is that i want to assign x directly, no subtracting like used in the example, when done from within the class, like from a method, but when the class setting the property isnt related to the current one, it does the subtracting. In the provided solution it will do the subtracting even when its assigned from a method of that same class, as long as i dotn use _x which for reasons of already having thousand lines of code i would need to change, i dont want to do – laundmo Jun 11 '19 at 23:53
2

A solution may consist in always using self.__dict__ inside the class without calling the __setattr__ method.

Example:

class myClass:

    def __init__(self, value):
        self.__dict__['a'] = value

    def __setattr__(self, name, value):
        print("called from outside")
        if name == 'a':
            self.__dict__[name] = value - 5
        else:
            self.__dict__[name] = value


f = myClass(10)

print(f.a)
# 10

f.a = 20
print(f.a)
# called from outside
# 15
abc
  • 11,579
  • 2
  • 26
  • 51
  • i have tried your solution but when setting self.a = 5 inside the class it still thinks its called from the outside – laundmo Jun 11 '19 at 20:13
  • You have to use `self.__dict__['a'] = 5` inside the class to differentiate, if you use `self.a = 5` you would call the `__setattr__` method – abc Jun 11 '19 at 20:17
  • well, another solution does exactly what i wanted, thank you for trying though – laundmo Jun 11 '19 at 20:21