Home > database >  Is it possible to create an object that actualize its attributes when modified?
Is it possible to create an object that actualize its attributes when modified?

Time:11-02

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 of a computational power issue, 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.

Now I found a not elegant solution to solve the issue with computational power:

class Hand:
    def __init__(self, fingers:list):
        self.fingers = fingers
        self.old_fingers = fingers
        self.number_of_fingers = len(fingers)

    def get_number_of_fingers(self):
        if self.fingers != self.old_fingers:
            self.old_fingers = self.fingers
            self.number_of_fingers = len(self.fingers)
        return self.number_of_fingers

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:

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 whenever fingers is modified, not every time the user asks for the attribute number_of_fingers.

Where do you need so much computing power?

  • Related