Uncategorized

Don’t use Python’s property


Published: December 21, 2023. Filed under:
Django, Python.

This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post for an introduction.

Attributing the problem

Suppose you’re writing Java and you write a class with an attribute :

public class MyClass {
    public int value;
}

And then later on you realize that value really wants to have some extra work happen at runtime — maybe it actually needs to be calculated “live” from other values, or needs auditing logic to note accesses, or similar. So what you actually want is something more like:

public class MyClass {
    private int value;

    public int getValue() {
        // Do the work and return the value...
    }
}

Except, at this point, you can’t turn the integer attribute someValue into an integer-returning method someValue() without forcing everything which used this class — which may be a lot of stuff, not all of which is yours — to adapt to this new interface and recompile.

So the standard recommendation in Java is never to declare a public attribute; always declare it private, and write methods to retrieve and set it (called “getters” and “setters”).

Python has a solution

In Python, on the other hand, you could write your class:

class MyClass:
    value: int

    def __init__(self, value: int):
        self.value = value

And then when you later decide you need value to be a method, you can do that without forcing anyone else to change their code that used your class, by declaring it as a “property”:

class MyClass:
    _value: int

    def __init__(self, value: int):
        self.value = value

    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, value: int):
        self._value = value

A property in Python lets you create a method — or up to three, for the operations to get, set and delete — which acts like a plain attribute. You don’t need to write parentheses to call it. For example, the following will work:

>>> m = MyClass(value=3)
>>> m.value
3
>>> m.value = 5
>>> m.value
5

But you (mostly) shouldn’t use it

If you have exactly the use case above — you started out with an attribute, and later it became more complex and now needs to be a method, but you don’t want to have to rewrite all code using that class — then you should use property. That’s what it’s for (and in other languages, too; C#, for example, has built-in syntax for declaring wrappers similar to Python’s property).

But way too much use of property just comes down to “this is a method, and always was a method, I just wanted it to look like an attribute for aesthetic reasons”. Which is not a good use.

For example:

import math


class Circle:
    center: tuple[float, float]
    radius: float

    def __init__(self, center: tuple[float, float], radius: float):
        self.center = center
        self.radius = radius

    @property
    def area(self):
        return math.pi * (self.radius**2)

    @property
    def circumference(self):
        return 2 * math.pi * self.radius

Using property in this way can be deeply misleading, since it creates the impression of simple attribute access when in reality there might be complex logic — or even things like database queries or network requests! — going on. This is a necessary trade-off sometimes for plain attributes that later turn into methods (and should still be documented when it occurs), but when there’s no technical need to do this, you shouldn’t do it. In the example above, area() and circumference() should just be plain methods, not properties.

The one potential exception is for more complex use of descriptors — and Python’s property is a relatively simple example of a descriptor — which let you create objects that emulate attribute behavior and offer fine-grained control of that behavior. Django’s ORM, for example, uses descriptors to let you read and assign fields of a model class in ways that look like plain attribute access although under the hood there may be data conversion or even delayed/deferred querying going on. And even that has its detractors: needing to remember to use select_related() or prefetch_related() to avoid numerous extra queries for related objects can be annoying, and is a common “gotcha” when working with Django’s ORM (SQLAlchemy, by contrast, sets the default relationship-loading behavior on the relationship).

In general, though, unless you really know what you’re doing with advanced descriptor use cases, or you have the very specific use case of turning a pre-existing attribute into a method, you probably should be avoiding property, and writing descriptors in general, in your Python code.

Also, if you’re coming from a language like Java, don’t interpret this as “always write getter/setter methods from the start, just wrap them in property” — always start with plain attributes, and only change to getter/setter methods if you need to later. The point of property is that you can do so without breaking the API of your class.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *