Tuesday, June 6, 2017

Properties in Python

Properties in Python
Encapsulation is an important concept in object-oriented programming. Encapsulation just means that some information is not available directly from outside the encapsulating object, only through specially provided accessor methods. This sounds like a pretty good idea at first glance: the implementation of something is hidden, and is allowed to change without breaking things, because people using this code aren’t able to poke around inside and count on details they’ve discovered, they have to use the accessor methods.

What is a property?

Here’s a contrived example, a fragmentary class (in Java, perhaps) describing a person, where we’ve only shown one piece of data, the age of the person:
class Person
{
    int age;
public:
    Person() : age(0) { }

    int getAge() { return this.age; }
    void setAge(int x) { this.age = x; }
};
Here the instance variable age is private, but we’ve also provided a pair of public methods which give access to the value of age: getAge to read it and setAge to set it. Now if something a lot more complicated needs to happen later to age the getter/setter methods can be updated but it doesn’t affect clients using this code. And some evolution is not not actually an unreasonable expectation - the age of a person isn’t a good candidate for data that looks static, since every year on their birthday it changes. So perhaps someday we’ll improve the class by calculating the age from the current date and the person’s birth date only when it is needed?
A lot of developers don’t like this kind of code; it does accomplish a useful function if you need it, but because you can’t necessarily guess when you’re going to need it, when you’re writing in a language like Java, and to a slightly lesser extent C++, you have to set up the getter/setter methods for all data members that are to look public up front, in anticipation that you might need the accessors later - since the interface has a binary (ABI) nature, you can’t change from a public instance variable to a private one with a getter/setter or you will break programs depending on the older class signature. This bloats the code in anticipation of something that might not actually needed, and no new value has been introduced by all those extra lines. Ah, but no problem, right? My IDE just auto-built those for me…
There’s an aesthetic concern regarding the syntax, also. A Person instance includes that person’s age, but we can’t perform natural operations on that age - if person is an instance, we can’t access person.age or set it, we have to use person.getAge() and person.setAge().
The C# language improves on this by providing properties. C# defines properties thus:
A property is a member that provides a flexible mechanism to read, write, or compute the value of a private field. Properties can be used as if they are public data members, but they are actually special methods called accessors. This enables data to be accessed easily and still helps promote the safety and flexibility of methods.
A simple example looks like this:
class Person
{
    private int age = 0;

    public int Age
    {
        get { return this.age; }
        set { this.age = value; }
    }
}
So if person is an instance of Person, person.Age (but not person.age) can be accessed externally as if it were a variable. That leads to the ability to write the much nicer
person.Age += 1
instead of
person.setAge(person.getAge() + 1)

Properties in Python

Python has properties too, but there is another benefit in Python: as a dynamic language, it does not have the limitation of static languages, you can change the implementation, without causing problems to clients because you’re not dealing with a compiled interface. This means you can define an instance variable first, then evolve it to a property later if needed, and it will not break clients.
Here is a series of examples showing how properties work in Python.
Consider a Vector class that should be able to provide both an angle in radians and an angle in degrees. This provides an excuse to use a getter method - we don’t actually need to store both angles in the instance, and indeed we don’t really want to, because they’re related, and if someone updates one angle, we have a problem because the other one needs to change in sync with it. It’s nicer to store one, and generate the other one on demand - that solves the sync problem.

Using the property function

Python provides the built-in property() function which sets up a property given arguments which describe the methods which implement the property behavior. The arguments are in order are the getter, setter, deleter, and docstring; they’re successively optional so if you pass only one argument to property only a getter is assigned.
import math


class Vector(object):
    def __init__(self, angle_rad):
        self.set_angle_rad(angle_rad)

    def get_angle_rad(self):
        return math.radians(self._angle_deg)

    def set_angle_rad(self, angle_rad):
        self._angle_deg = math.degrees(angle_rad)

    angle = property(get_angle_rad, set_angle_rad)

    def get_angle_deg(self):
        return self._angle_deg

    def set_angle_deg(self, angle_deg):
        self._angle_deg = angle_deg

    angle_deg = property(get_angle_deg, set_angle_deg)
We can do some experiments with this class - in the first set of lines below we create an instance with a starting value and print both angles, then change the first the angle then the angle_deg values to show they’re working in unison. In the final chunk, we ask for some information about the objects in question to illustrate how Python has set this up.
v = Vector(2*math.pi)
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

v.angle = math.pi
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

v.angle_deg = 90.0
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

print(Vector.angle, Vector.angle.getter, Vector.angle.setter)
print(Vector.angle_deg, Vector.angle_deg.getter, Vector.angle_deg.setter)
Here’s the output of one run:
Rad: 6.283185307179586, Deg: 360.0
Rad: 3.141592653589793, Deg: 180.0
Rad: 1.5707963267948966, Deg: 90.0
<property object at 0x7fab853b5f48>
  <built-in method getter of property object at 0x7fab853b5f48>
  <built-in method setter of property object at 0x7fab853b5f48>
<property object at 0x7fab7d3d9818>
  <built-in method getter of property object at 0x7fab7d3d9818>
  <built-in method setter of property object at 0x7fab7d3d9818>

Using the property decorators

Python provides decorators that have the same effect as the the call to the property function. @property is used for the getter, @x.setter for the setter and @x.deleter for the deleter method which would be the third argument to the property function if included (replace x with the method name).
import math


class Vector(object):
    def __init__(self, value):
        self.angle = value

    @property
    def angle(self):
        return math.radians(self._angle_deg)

    @angle.setter
    def angle(self, value):
        self._angle_deg = math.degrees(value)

    @property
    def angle_deg(self):
        return self._angle_deg

    @angle_deg.setter
    def angle_deg(self, value):
        self._angle_deg = value

v = Vector(2*math.pi)
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

v.angle = math.pi
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

v.angle_deg = 90.0
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

print(Vector.angle, Vector.angle.getter, Vector.angle.setter)
print(Vector.angle_deg, Vector.angle_deg.getter, Vector.angle_deg.setter)
And the output of our experiments:
Rad: 6.283185307179586, Deg: 360.0
Rad: 3.141592653589793, Deg: 180.0
Rad: 1.5707963267948966, Deg: 90.0
<property object at 0x7f7ba29b5818>
  <built-in method getter of property object at 0x7f7ba29b5818>
  <built-in method setter of property object at 0x7f7ba29b5818>
<property object at 0x7f7ba29b5868>
  <built-in method getter of property object at 0x7f7ba29b5868>
  <built-in method setter of property object at 0x7f7ba29b5868>
By decorating the angle and angle_deg method pairs, we’ve turned them into properties with getter/setter methods, just like the call to the property function did, but this looks cleaner, you can immediately see what each method is for rather than going hunting to see they’re later part of a property call. Notice that the method names have to be the same for all the parts of the property; for the setter and deleter the decorator also takes the name of the method.

Code Simplification

I don’t particularly like this code, though. We are using a sort of hidden instance variable as the backing field which holds the value, and we’ve served up getter/setter pairs for both public variables. Except there is really no hidden data in Python - starting a name with an underscore is a visual hint that we don’t intend something to be public, but that is all it is, a hint (a leading single underscore only "matters" in imports). That means someone could actually fiddle directly with the backing field _angle_deg, bypassing the getter/setter, if they were so motivated. In the trivial example here, that doesn’t introduce any new problems, but in a setter which does a bunch of validation so you know an invalid value is never stored, it is not ideal. And in fact, that the setter for angle_deg does not do anything special is my other complaint: why implement a getter/setter when there is no need to?
So why not undo the property definition that does not seem needed and just make angle_deg an instance variable, then we don’t need _angle_deg at all. If we find we need to do something "special" with angle_deg later we can always turn it back into a property. Notice in the initializer, we are invoking the property setter, because we assign to angle. As a next refactor, I would probably turn this around and use the radians form as the instance variable to make it all feel more natural. This is the Python flexibility I was referring to at the beginning of this article. Here’s the refactored code, which is now quite a bit shorter:
import math


class Vector(object):
    def __init__(self, value):
        self.angle = value

    @property
    def angle(self):
        return math.radians(self.angle_deg)

    @angle.setter
    def angle(self, value):
        self.angle_deg = math.degrees(value)

v = Vector(2 * math.pi)
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

v.angle = math.pi
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))

v.angle_deg = 90.0
print("Rad: {}, Deg: {}".format(v.angle, v.angle_deg))
This works just the same, as we see from the output:
Rad: 6.283185307179586, Deg: 360.0
Rad: 3.141592653589793, Deg: 180.0
Rad: 1.5707963267948966, Deg: 90.0

No comments:

Post a Comment