Reputation: 21969
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()
Upvotes: 9
Views: 2062
Reputation: 25833
You're on the right track, you want to make x
a property instead of having it be an attribute in the subclass. Where you went wrong was trying to store the raw data for x
on super. What you want to do is exploit the fact that the parent class can use the new property of the subclass transparently and does not need to know that it is now a property and not a attribute. Something like this should work for you:
class Foo(object):
def __init__(self, x):
self.x = x
class Bar(Foo):
_protected_x = None
@property
def x(self):
return self._protected_x
@x.setter
def x(self, value):
if self._protected_x is None:
self._protected_x = value
else:
raise ValueError("Use set_x to change x.")
def set_x(self, value):
self._protected_x = value
b = Bar(12)
print b.x
b.set_x(5)
print b.x
Upvotes: 0
Reputation: 6738
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.
Upvotes: 1
Reputation: 22041
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()
Upvotes: 1