-2

Today at work I had a session where I explained properties in Python to data science colleagues who were not very familiar with OOP in Python. Among the questions I received, there was the simple issue of why in Python the setter/getter situation is a bit unintuitive.

For example, if you have field in a class it is an expected behaviour that a method prefixed with @property will return a value of said field:

class Bar:
    def __init__(self, a: int):
        self._a = a
        
    @property
    def a(self):
        return self._a

And then:

f = Bar(2)
print(f.a) # Out: 2

But what if you have a dedicated getter, like you would in other languages like Java or C#, it is sensible when you are trying to get a value of an attribute:

class Baz:
    def __init__(self, a: int):
        self._a = a
        
    @property
    def a(self):
        return None
    
    @a.getter
    def a(self):
        return self._a

And if we do:

bb = Baz(2)
print(bb.a)  # Out: 2

Here it returns 2 despite the fact that the field prefixed with @property supposedly returns None.

Obviously, for people in the know the fact that @property returns a value and the fact than getter is redundant in Python syntax is hardly an issue. And of course, the following doesn't work:

class Booz:
    def __init__(self, a: int):
        self.a = a
        
    @a.getter
    def a(self):
        return self.a

bbb = Booz(2) # Out: NameError: name 'a' is not defined

Is there a particular reason why fields in Python have to be defined in this unintuitive fashion, i.e. you would expect to get the value of a field from a getter and you would expect to set a value of a field with a setter. What is the inherent necessity for @propery?

Why is the following (Java-like) syntax not possible in Python:

class Booz:
    a: int 
        
    @a.getter
    def a(self):
        if isinstance(a, int):
            return self.a
NotAName
  • 3,821
  • 2
  • 29
  • 44
  • Related: [What's the pythonic way to use getters and setters?](https://stackoverflow.com/questions/2627002/whats-the-pythonic-way-to-use-getters-and-setters) – jarmod Aug 03 '22 at 17:43
  • 2
    Only `@property` is needed to make a getter, so I don't see why the rant about `@a.getter`. – gre_gor Aug 03 '22 at 17:43
  • 4
    The short answer is, because it was shoehorned into Python (in v2.2). This was the way for Python to get accessors without radically changing the parser. `@property` wraps the function and creates the accessor. The accessor (since v2.6) has ways to modify the accessor (more precisely, replace the accessor with a new, updated one). `@a.setter` redefines the setter. `@a.deleter` redefines the deleter. And `@a.getter` redefines the getter; nothing sinister about it. Normally you don't need to redefine the getter, so `@a.getter` is almost never used. – Amadan Aug 03 '22 at 17:46
  • @gre_gor, but the only thing `@property` does in the standard synstax is return the value of a field. In that case the naming is confusing. Why not just call it `@getter`? – NotAName Aug 03 '22 at 17:49
  • Confusing with what? – gre_gor Aug 03 '22 at 17:52
  • @gre_gor, `@property` doesn't necessarily say that it is a way to *get* a value like a *getter* does. If you call a `getter` it is expected that you would *get* a value in return. Does `@property` intuitively say the same? No, it doesn't. – NotAName Aug 03 '22 at 17:55
  • 1
    Because property is an object which gives an instance the ability to execute certain functions without the need for parentheses, imitating simple attributes. `bar.a` is normally a value, and `bar.a = 3` would normally replace the value with `3`. If `bar.a` is a property, `bar.a` invokes the getter function instead (whereas you would have to write `bar.a()` otherwise), `bar.a = 3` invokes the setter function, and `del bar.a` the deleter. This dispatching is done by the property. `@property` by default only defines the getter, but the property object itself is responsible for all three. – Amadan Aug 03 '22 at 17:58
  • @Amadan, yes but why the `@property` decorator by default acts as a getter when you have a dedicated `getter` included in the syntax? – NotAName Aug 03 '22 at 18:00
  • 1
    Because of the way `property` was defined: `property(fget=None, fset=None, fdel=None, doc=None)`. Remember, this was the original way to use it, before decorators: a property object defines all three accessor methods. Now, using a function as a decorator (added later) will take the decorated function and give it as the first parameter: so `@property; def a...` is equivalent to `def a...; a = property(a)`. Take a look again at what the first argument of `property` is: a getter. It is a design that makes use of existing syntax to do a new thing, with minimal impact on existing code. – Amadan Aug 03 '22 at 18:09
  • You could also do `a_prop = property(); @a_prop.getter; def a...` which might be more intuitive for you, but that produces an extra field (`a_prop`) that is not needed. But this illustrates that `property` is a class with methods that update different accessor functions, and not just a confusing way to call a getter. – Amadan Aug 03 '22 at 18:15
  • @Amadan, thanks. This makes sense even if on pure reading of the code (assuming the reader is not familiar with Python) it still doesn't make intuituve sense. But at least I have a better idea of how it came to be this way. – NotAName Aug 03 '22 at 18:15

1 Answers1

4

Remember that properties were a band-aid added later, with some resistance. It violates the "one way to do things" rule. The original implementation was like this:

class Baz:
    def __init__(self, a: int):
        self._a = a
        
    def set_a(self):
        return None
    
    def get_a(self):
        return self._a

    a = property(set_a, get_a)

This is somewhat sensible and orthogonal. The decorator syntax was added a bit later. The implementation is that the @property decorator creates the a property and populates either the getter or setter. The @a.getter syntax modifies the property that had just been created by filling in the other field.

So, using the property is like saying

    a = property()
    a = a.setter(set_a)
    a = a.getter(get_a)

and I believe that's actually how the decorators are implemented.

Tim Roberts
  • 48,973
  • 4
  • 21
  • 30