Advantage of @property assuming I only need to get the value

Viewed 67

I have read a lot about the advantages of @property, and I really like them. As a short example

class Foo:
    def __init__(self):
        self._foo_value = 'foo_value'

    @property
    def foo_value(self):
        return self._foo_value


my_foo = Foo()
print(my_foo.foo_value)

However, assuming I know that I don't want do fancy stuff and only get the value of Foo.foo_value, I can achieve the same behavior with less code by ignoring @property, e.g.

class Foo:
    def __init__(self):
        self.foo_value = 'foo_value'


my_foo = Foo()
print(my_foo.foo_value)

Is there any advantage of still using the @property in such a case?

2 Answers

No, there is absolutely no use for a property here. The entire point of property is to avoid boilerplate and maintain encapsulation.

A Pythonic class definition

Suppose you write a class:
class Circle:
    def __init__(self, radius):
        self.radius = radius

Great. Now, there's a bunch of client code out there like:

print(f"The radius is {some_circle.radius}")

An un-Pythonic class definition

Suppose, however, you want to make sure the radius is never set to a value below 0. Oh no! What should we do? The philosophy in languages like Java is that you should have written boilerplate getters and setters like this in the first place:

class Circle:
    def __init__(self, radius):
        self.set_radius(radius)
    def get_radius(self):
        return self.radius
    def set_radius(self, radius):
        self._radius = radius

Then, all the client code would have been written like:

print(f"The radius is {some_circle.get_radius()}")

And we can just modify set_radius to throw an appropriate error:

class Circle:
    def __init__(self, radius):
        self.set_radius(radius)
    def get_radius(self):
        return self.radius
    def set_radius(self, radius):
        if radius < 0:
            raise ValueError("Radius must be positive")
        self._radius = radius

All that boilerplate at the beginning was worth it because now we can make changes to the way we control access to our internal state without breaking client code.

But this isn't Java, this is Python

In Python, if and when we decide to control access, we can refactor our class without breaking client code!:

class Circle:
    def __init__(self, radius):
        self.radius = radius
    @property
    def radius(self):
        return self._radius
    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("Radius must be positive")
        self._radius = radius

Because let's be honest. Most of the time we'd rather write a class like the first example, not the second example with the pre-emptive boilerplate getters and setters, and we like to write client code like:

pi * circle.radius**2

instead of

pi * circle.get_radius()**2

In Python, we can have our cake and eat it too.

An exception

There is one use case for a property like in your code: we want to let the clients of our class retrieve a value but not set the value. So just like your example:

>>> class Foo:
...     def __init__(self):
...         self._foo_value = 'foo_value'
...     @property
...     def foo_value(self):
...         return self._foo_value
...
>>> foo = Foo()
>>> foo.foo_value
'foo_value'
>>> foo.foo_value = 'something else'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

If he above is the behavior you want, the the property is actually serving a purpose. Of course, this can be easily circumvented, but it does prevent "accidental" misuse of the API. And, we still get to keep the nicer-looking foo.foo_value instead of the uglier foo.get_foo_value()

There is no advantage. If anything, you are just creating baggage for yourself.

Other languages (e.g. Java) implement "getters" and "setters" and have things like private variables. There is no such thing as a private variable in Python; you could use a leading _ to indicate that a name is private, or even use __ for "name mangling" (but that still won't stop a determined user).

There are times that @property does have some great utility. An uncontrived example is in the config class of one of my wrapper libraries. I know that a value outside of 0 to 1 is going to crash another API call, so I try to stop users from getting in a confusing mess if that call would blow up. I can't actually stop them doing it anyway, but I can try make the API warn them.

(Side note; I don't suggest that function names should be in CAPS but that's actually the name of some Java attributes that the API call will set, so it's for consistency)

    @property
    def CLUSTER_REGRET(self):
        return self._CLUSTER_REGRET

    @CLUSTER_REGRET.setter
    def CLUSTER_REGRET(self, value):
        if not 0 <= value <= 1:
            raise ValueError("CLUSTER_REGRET must be between 0 and 1")
        self._CLUSTER_REGRET = value
Related