Say I have an class that looks like this:
class Test(object):
def __init__(self, a, b):
self.a = a
self.b = b
self.c = self.a + self.b
Yes, there is! It's called properties.
class Test(object):
def __init__(self,a,b):
self.a = a
self.b = b
@property
def c(self):
return self.a + self.b
With the above code, c
is now a read-only property of the Test
class.
You can also give a property a setter, which would make it read/write and allow you to set its value directly. It would look like this:
class Test(object):
def __init__(self, c = SomeDefaultValue):
self._c = SomeDefaultValue
@property
def c(self):
return self._c
@c.setter
def c(self,value):
self._c = value
However, in this case, it would not make sense to have a setter for self.c
, since its value depends on self.a
and self.b
.
The @property
bit is an example of something called a decorator. A decorator actually wraps the function (or class) it decorates into another function (the decorator function). After a function has been decorated, when it is called it is actually the decorator that is called with the function (and its arguments) as an argument. Usually (but not always!) the decorated function does something interesting, and then calls the original (decorated) function like it would normally. For example:
def my_decorator(thedecoratedfunction):
def wrapped(*allofthearguments):
print("This function has been decorated!") #something interesting
thedecoratedfunction(*allofthearguments) #calls the function as normal
return wrapped
@my_decorator
def myfunction(arg1, arg2):
pass
This is equivalent to:
def myfunction(arg1, arg2):
pass
myfunction = my_decorator(myfunction)
So this means in the class example above, instead of using the decorator you could also do this:
def c(self):
return self.a + self.b
c = property(c)
They are exactly the same thing. The @property
is just syntactic sugar to replace calls for myobject.c
with the property getter and setter (deleters are also an option).
You might be wondering why simply doing this once:
myfunction = my_decorator(myfunction)
...results in such a drastic change! So that, from now on, when calling:
myfunction(arg1, arg2)
...you are actually calling my_decorator(myfunction)
, with arg1, arg2
sent to the interior wrapped
function inside of my_decorator
. And not only that, but all of this happens even though you didn't even mention my_decorator
or wrapped
in your function call at all!
All of this works by virtue of something called a closure. When the function is passed into the decorator in this way (e.g., property(c)
), the function's name is re-bound to the wrapped version of the function instead of the original function, and the original function's arguments are always passed to wrapped
instead of the original function. This is simply the way that closures work, and there's nothing magical about it. Here is some more information about closures.
So to summarize so far: @property
is just a way of wrapping the class method inside of the property()
function so the wrapped class method is called instead of the original, unwrapped class method. But what is the property function? What does it do?
The property function adds something called a descriptor to the class. Put simply, a descriptor is an object class that can have separate get, set, and delete methods. When you do this:
@property
def c(self):
return self._c
...you are adding a descriptor to the Test
class called c
, and defining the get method (actually, __get__()
) of the c
descriptor as equal to the c(self)
method.
When you do this:
@c.setter
def c(self,value):
self._c
...you are defining the set method (actually, __set__()
) of the c
descriptor as equal to the c(self,value)
method.
An amazing amount of stuff is accomplished by simply adding @property
to your def c(self)
method! In practice, you probably don't need to understand all of this right away to begin using it. However, I recommend keeping in mind that when you use @property
, you are using decorators, closures, and descriptors, and if you are at all serious about learning Python it would be well worth your time to investigate each of these topics on their own.
The simplest solution is to make c
a read-only property:
class Test(object):
def __init__(self, a, b):
self.a = a
self.b = b
@property
def c(self):
return self.a + self.b
Now every time you access test_instance.c
, it calls the property getter and calculates the appropriate value from the other attributes. In use:
>>> t = Test(2, 4)
>>> t.c
6
>>> t.a = 3
>>> t.c
7
Note that this means that you cannot set c
directly:
>>> t.c = 6
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
t.c = 6
AttributeError: can't set attribute