gionni
gionni

Reputation: 1303

Python attribute that is read only within class

I have a class:

class Portfolio:
    def __init__(self, value):
        self.value = value

class GenericStrategy:
    def __init__(self, portfolio: Portfolio):
        self.portfolio = portfolio

    def modify_ptf_value(new_value):
        self.portfolio.value = new_value
        # This should return an error

I'will write some strategies which will inherit from GenericStrategy. I'd like their methods to be able to read the attribute portfolio but not to modify it, nor its attributes.

I read something about the @properties decorator, but it only works if I don't want the attribute (and its attributes) to be accessible from the outside, i can still modify the attribute (and its attributes) from methods 'inside' the object. Is there a way to make the attribute (and its attributes) 'read-only' except for the __init__ method? Is my design wrong and should start over? I know it is up to the user not to modify "protected" attributes, but I would like to make it bullet proof. Any idea is well accepted, even if it requires a substantial change in the class design.

Thanks

Upvotes: 0

Views: 23489

Answers (1)

CristiFati
CristiFati

Reputation: 41112

As opposed to other (commonly used) programming languages Python comes with a new approach regarding accessing class/instance members. For example, nothing is really private, the fields/methods that:

  • start with an _, are regular fields
  • start with __ (and end with at most one _), are just name mangled, but they still can be accessed (even modified/deleted) from outside the class

So, at the end it's a matter of convention, and it relies that it will be followed by those who write code. Bottom line is there's nothing that would prevent an user gaining access to a class/instance's internals.

Note: In other language it's possible too to access private members: there are methods officially supported (like Reflection ([Oracle]: Trail: The Reflection API) for Java), or not so officially supported (which require some "tricks" - e.g.: reinterpret_casting a class to a struct with the same structure for C++). Nowadays, more and more languages tend to offer a way to alter an instance structure.

Anyway, there is the so called Descriptor Protocol ([Python]: Descriptor HowTo Guide) which is one of the Python's most powerful (and also most misunderstood) features.

Using descriptors (as a side comment, properties rely on them), I wrote a piece of code that achieves (on some degree) what you're asking for:

class LockedAttribute(object):
    def __init__(self, name):
        self._name = name
        self._set_count = 0
        self._set_treshold = 1

    def __get__(self, instance, cls):
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if self._set_count >= self._set_treshold:
            raise AttributeError("Can't set attribute '{}'".format(self._name))
        else:
            instance.__dict__[self._name] = value
            self._set_count += 1

    def __delete__(self, instance):
        raise AttributeError("Can't delete attribute '{}'".format(self._name))


class GenericStrategy(object):
    portfolio = LockedAttribute("portfolio")

    def __init__(self, portfolio):
        self.portfolio = portfolio
        try:
            self.portfolio = portfolio
        except AttributeError as e:
            print("  ERROR: {}".format(e))

    def set_portfolio(self, new_value):
        self.portfolio = new_value


if __name__ == "__main__":
    strategy = GenericStrategy("some portfolio name")
    print("Portfolio: {}".format(strategy.portfolio))
    try:
        del strategy.portfolio
    except AttributeError as e:
        print("  ERROR: {}".format(e))
    try:
        strategy.set_portfolio("some 2nd portfolio name")
    except AttributeError as e:
        print("  ERROR: {}".format(e))
    try:
        strategy.portfolio = "some 3rd portfolio name"
    except AttributeError as e:
        print("  ERROR: {}".format(e))
    print("Portfolio: {}".format(strategy.portfolio))

Notes:

  • I removed __ from the private attribute's name (portfolio) to avoid using the mangling that I was talking about, in my code (would make it more difficult to read)
  • For Py27 compatibility, the classes inherit object. If the compatibility isn't mandatory, the inheritance relation can be deleted (in Py3x it is by default)
  • I added the try/except blocks to illustrate the behavior, in production they should be deleted
  • As I stated above, if anyone wants to alter this behavior, can

Upvotes: 1

Related Questions