Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent access to an instance variable from subclass, without affecting base class

Say I have a simple class Foo, which comes from an external library, thus I cannot change it directly:

class Foo(object):
    def __init__(self, x):
        self.x = x

I want to create a subclass Bar and prevent x from being change from an instance of Bar, but still use the x in Bar's methods.

Here's what I tried, and it will probably enlighten the basic idea, but unfortunately it doesn't work:

class Bar(Foo):

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

    @x.setter
    def x(self, value):
        raise NotImplementedError('Do not change x directly, use "do_stuff()" instead')

    def do_stuff(self, value):
        if <something>:
            super().x = value

So basically I've created some wrapper functions (do_stuff()) around an attribute, and now I want to prevent the attribute from being changed directly, as it might mess up some functionality of the wrapper functions. Is this possible in a reasonable way?

Edited with a better example of what I want. I'm not trying to prevent them from seeing the variable x, but instead changing it from outside of do_stuff()

like image 237
Markus Meskanen Avatar asked Oct 27 '15 12:10

Markus Meskanen


People also ask

Does a subclass inherit instance variables?

Subclasses inherit public methods from the superclass that they extend, but they cannot access the private instance variables of the superclass directly and must use the public accessor and mutator methods.

Can we override instance variables of a superclass?

Like others have mentioned, you cannot override a super class's instance variables, but you can use constructors to assign the proper values for your objects.

Do child classes inherit variables?

We know every Child class inherits variables and methods (state and behavior) from its Parent class.

Are instance variables inherited in Java?

I know that instance variable are not inherited but they can be accessed in sub class. If they can be accessed in sub class then does that means that they are shared between super class and subclass or both super class and subclass have different copy.


2 Answers

This should be much simpler to accomplish if you are willing to avoid inheritance altogether:

def main():
    bar = Bar(123)
    bar.fizz()
    bar.buzz()
    bar.fizz()
    bar.set_x(456)
    print('bar.x =', bar.x)
    try:
        bar.x = 123
    except AttributeError:
        print('bar.x cannot be set directly')
    else:
        raise AssertionError('an AttributeError should have been raised')
    bar.mutate_x(789)
    bar.fizz()
    bar.set_x(0)
    bar.fizz()
    bar.mutate_x(1)
    bar.fizz()
    bar.set_x('Hello World')
    bar.fizz()


class Foo:

    def __init__(self, x):
        self.x = x

    def fizz(self):
        print(self.x)

    def buzz(self):
        self.x = None


class Bar:

    def __init__(self, x):
        self.__foo = foo = Foo(x)
        self.__copy_methods(foo)

    def __copy_methods(self, obj):
        for name in dir(obj):
            if name.startswith('__') or name.endswith('__'):
                continue
            attr = getattr(obj, name)
            if callable(attr):
                setattr(self, name, attr)

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

    def set_x(self, value):
        if isinstance(value, int) and value > 0:
            self.__foo.x = value

    mutate_x = set_x

if __name__ == '__main__':
    main()
like image 119
Noctis Skytower Avatar answered Sep 21 '22 05:09

Noctis Skytower


The short answer is: No, this is not possible in a reasonable way.

Python's guiding principle here, to use the phrasing from the style guide is that we are all responsible users. Meaning that code is trusted not to do silly things, and people should generally avoid messing with members of other people's classes without a good reason.

The first and best way to prevent people from accidentally changing a value is to mark it using the single underscore (_variable). This however may not offer you the protection you want against accidental modification of your variables.

The next step up in protection is to use a double underscore. Quoting from PEP-8:

To avoid name clashes with subclasses, use two leading underscores to invoke Python's name mangling rules.

Python mangles these names with the class name: if class Foo has an attribute named __a , it cannot be accessed by Foo.__a . (An insistent user could still gain access by calling Foo._Foo__a .) Generally, double leading underscores should be used only to avoid name conflicts with attributes in classes designed to be subclassed.

The mangling makes it more difficult to accidentally overwrite a value.

I added emphasis to that last sentence because it is important. Using this mechanism for preventing accidental access to a member is not really the something that should be done for a lot of members.

In your specific case, the way that I'd solve the problem would be to not subclass at all. Consider:

class Foo(object):
    def __init__(self, x):
        self.x = x

class Bar():
    def __init__(self, x):
        self._foo = Foo(x)

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

    def do_stuff(self, value):
        # Validate the value, and the wrapped object's state
        if valid:
            self._foo.x = value

Of course this means that Bar has to wrap all of Foo's methods that you want to wrap. Yes, someone could still,

b = Bar(100)
b._foo.x = 127 # shame on them :)

or

b = Bar(100)
b._foo = EvilFoo(127)

but it's harder to unintentionally do.

like image 33
theB Avatar answered Sep 18 '22 05:09

theB