newb question about @property

Steve D'Aprano steve+python at pearwood.info
Sun Oct 1 01:25:28 EDT 2017


On Sun, 1 Oct 2017 11:07 am, Bill wrote:

> You and Ned are both right.  Up until a few minutes ago, I wasn't
> thinking about a class having more than 1 attribute that I wanted to
> change.  And now I realize that __get__ doesn't really make sense in
> that context (in the back of my mind was the notion that @property
> defined __get__, __set__ and __del__) and I allowed that to obscure my
> vision.   I was on the verge of giving up anything to do with computers,
> forever.  : )


property *does* define __get__ and __set__, but not __del__, rather it is called
__delete__. Confusing, I know, but neither __del__ nor __delete__ should be
used very often.

__del__ is the instance destructor. When you have an object:

obj = Some_Class()

and the last reference to that object goes out of scope, the garbage collector
collects the object and reclaims its memory. Before doing so, it calls
obj.__del__ if it exists.

(That's a simplified version.)

__delete__ on the other hand is part of the descriptor protocol, which is how
Python manages:

- methods
- classmethod and staticmethod
- property

and more. Generally speaking, the descriptor protocol is considered "advanced",
not as advanced or scary as metaclasses, but not for beginners either. However,
using property is much less complex.

Properties are *computed attributes*. Here is a grossly simplified explanation
of how Python does attribute look-ups, closer to what Python did in version 1.5
than what it does now, but its a good place to start.

There are three things you can so with an attribute: get it, delete it, or set
it. (A) When you try getting an attribute:

    result = obj.spam

the interpreter starts by looking in the object's instance namespace for a
key 'spam':

    obj.__dict__['spam']

If it finds it, it returns the associated value and we're done. If not, it next
looks at the object's class:

    obj.__class__.__dict__['spam']

and if not, then it looks in any superclasses, then it looks for a __getattr__
method, and finally if all else fails it raises AttributeError.

(B) Deleting an attribute:

    del obj.spam

is pretty much the same.

(C) When you try setting an attribute:

    obj.spam = eggs

the interpreter assigns to the instance namespace:

    obj.__class__.__dict__['spam'] = eggs



So that's (roughly) how Python worked back in 1998 or so, and its still
conceptually close to what happens now. Now let's introduce an extra layer of
complexity: properties[1].

(A) if the result of looking up obj.spam is a property object, then instead of
returning the property object itself, the interpreter calls the property's
getter method, and returns what it returns.

(B) Likewise, deleting the property calls the property's deleter method. (Its
rare to bother with one of them, so in practice you can ignore it.)

(C) And setting obj.spam to a new value calls the property's setter method,
which is intended to handle setting the value somewhere.


(Again, I'm ignoring much of the complexity needed to by the actual
implementation, in order to focus on just the basic conceptual steps.)


 
> BTW, your example (below) is very nice!  I may have seen something
> similar before, but I am starting to appreciate it better now.  I think
> all of this would have made a bit more sense (to me), if instead of just
> "@property", the syntax was "@property.getter".


One of the most common use for properties is read-only attributes, so the
decision was made long ago to have property alone to be equivalent to
property.getter. But if you feel the need, you can write:

@property.getter
def spam(self):
    ...


but if you do, expect people to look at you funny and say "what's that do?".


> Now I am forced to ask 
> the question, why did they use the underscore (on temperature) in the
> example on the bottom of this page? Is one forced to introduce new
> identifiers in order to define a setter?

Forced to? Not exactly. A computed attribute need not have any storage at all,
or it could only use public attributes, like my earlier Circle example. Circle
didn't use any setters, but I could have let you set the diameter, which in
turn would set the radius:

circle.radius = 2
assert circle.diameter == 4
circle.diameter == 2  # requires a setter
assert circle.radius == 1

Getting that to work is left as an exercise :-)

But most commonly, computed attributes need to store some data aside, somewhere.
You could use a global variable, or write it to a file, or stick it in a list.
All of these things have obvious problems, so the most sensible approach it to
stick the data in a private attribute.

The interpreter doesn't enforce notions of private/public when it comes to
Python classes, but there's a very strong convention that anything starting
with a single underscore is private.



[1] Technically, the interpreter knows nothing about properties. What it cares
about is *descriptors*. Properties are just one kind of descriptor, as are
methods. But I'm intentionally not talking about the gory details of
descriptors. Feel free to ask if you care, but honestly, you don't need to care
unless you are writing your own descriptor class.


-- 
Steve
“Cheer up,” they said, “things could be worse.” So I cheered up, and sure
enough, things got worse.




More information about the Python-list mailing list