In this example, what should be done so that print(left_hand.number_of_fingers)
returns 4
and not 5
?
class Hand:
def __init__(self, fingers:list):
self.fingers = fingers
self.number_of_fingers = len(fingers)
left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
left_hand.fingers.pop()
print(left_hand.number_of_fingers) # I want this to actualize and be 4, not 5
I found a solution using @property
class Hand:
def __init__(self, fingers:list):
self.fingers = fingers
@property
def number_of_fingers(self):
return len(self.fingers)
But I'm not satisfied, because if computing number_of_fingers
was expensive we would only want to compute it whenever fingers
is modified, not every time the user asks for the attribute number_of_fingers
.
CodePudding user response:
So here in the first code (without using @property
), you will get the output as 5 and not 4, because you are simply assigning the value of len(fingers)
to number_of_fingers
attribute while initialising a Hand
object, and number_of_fingers
attribute is not getting linked to fingers
.
So even if left_hand.fingers
is modified in between the code, it will have no effect on the value of number_of_fingers
. One cannot change this behaviour.
Also you don't need that @property
, I tested and found that there will be no error if it is not written.
And finally coming to
But I'm not satisfied, because if computing
number_of_fingers
was expensive we would only want to compute it wheneverfingers
is modified, not every time the user asks for the attributenumber_of_fingers
.
Where do you need so much computing power?
CodePudding user response:
The problem is that the underlying list in your Hand
class, i.e. self.fingers
, is not sufficiently encapsulated so that any user can be modifying it, for example by calling left_hand.fingers.pop()
or even by assigning to it a new list. Therefore, you cannot assume that it has not been modified between calls to number_of_fingers
and therefore you have no choice but to compute its length in that call.
The solution is to control what clients of your class can and cannot do. The easiest way to do this is by using name mangling. That is, you prefix your attribute names with two leading underscore characters. This makes it difficult (although not impossible) for clients of your class to access these attributes from outside of the class (we assume that your users are not intentionally malicious). And therefore we have to provide now a pop
method:
class Hand:
def __init__(self, fingers:list):
self.__fingers = fingers
self.__number_of_fingers = len(fingers)
def pop(self):
assert(self.__fingers)
self.__number_of_fingers -= 1
return self.__fingers.pop()
@property
def number_of_fingers(self):
return self.__number_of_fingers
left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
print(left_hand.pop())
print(left_hand.number_of_fingers)
Prints:
pinkie
4
I am not suggesting that you actually do the following, but if you wanted to you can get more elaborate by creating special class decorators @Private
and @Public
that will wrap your class in a new class and check access to your attributes ensuring that you are not accessing those attributes defined to be private. You use either the @Private
decorator to define those attributes/methods that are private (everything else is considered public) or the @Public
decorator to define those attributes/methods that are public (everything else is considered private), but not both. You would typically name your private attributes with a leading single underscore, which is the convention that tells users that the attribute/method is to be considered private.
This is meant more to catch inadvertent access of attributes that are meant to be private. If you execute the code with the -O Python flag, then no runtime checks will be made.
def accessControl(failIf):
def onDecorator(aClass):
if not __debug__:
return aClass
else:
class onInstance:
def __init__(self, *args, **kargs):
self.__wrapped = aClass(*args, **kargs)
def __getattr__(self, attr):
if failIf(attr):
raise TypeError('private attribute fetch: ' attr)
else:
return getattr(self.__wrapped, attr)
def __setattr__(self, attr, value):
if attr == '_onInstance__wrapped':
self.__dict__[attr] = value
elif failIf(attr):
raise TypeError('private attribute change: ' attr)
else:
setattr(self.__wrapped, attr, value)
return onInstance
return onDecorator
def Private(*attributes):
return accessControl(failIf=(lambda attr: attr in attributes))
def Public(*attributes):
return accessControl(failIf=(lambda attr: attr not in attributes))
@Private('_fingers', '_number_of_fingers')
class Hand:
def __init__(self, fingers:list):
self._fingers = fingers
self._number_of_fingers = len(fingers)
def pop(self):
assert(self._fingers)
self._number_of_fingers -= 1
return self._fingers.pop()
@property
def number_of_fingers(self):
return self._number_of_fingers
left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
print(left_hand.pop())
print(left_hand.number_of_fingers)
# Thsis will throw an exception:
print(left_hand._fingers)
Prints:
pinkie
4
Traceback (most recent call last):
File "C:\Booboo\test\test.py", line 50, in <module>
print(left_hand._fingers)
File "C:\Booboo\test\test.py", line 9, in __getattr__
raise TypeError('private attribute fetch: ' attr)
TypeError: private attribute fetch: _fingers
Prints: