Using Properties to Validate an Input at the Time a Class Instance is Created
The purpose of this blog post is to discuss some fundamental guidelines for creating class properties, namely: (1) the need to use getter and setter functions to validate inputs, and (2) the use of different variables for class properties and attributes. I did not have a clear understanding of those two things when I was first learning how to use class properties in python, so my attempts to use class properties were more frustrating than they needed to be. In my discussion of these points, I will refer to the following example:
Example 1:
class Person:
def __init__(self, name):
self.name = name
def get_name(self):
return self._name
def set_name(self, new_name):
if type(new_name) == str and len(new_name) > 0:
self._name = new_name
name = property(get_name, set_name)
Example 1 establishes a Person class which is meant to satisfy these conditions:
- An instance of the Person class must be given a name at the time of creation.
- The property functions are used to ensure that a person's name is a string containing at least one character.
- Calling the person's name should return the person's name.
If an instance of the Person class is created with a valid name,
> p1 = Person("Frank")
calling the instance's name produces the following result:
> p1.name
'Frank'
Creating an instance of the Person class with an invalid name and then calling the instance's name produces an error:
> p2 = Person(2)
> p2.name
*** AttributeError: 'Person' object has no attribute '_name'
I am pointing out these conditions because you can find many other good examples of how to use class properties, but they will not necessarily work the same way that Example 1 does. When I was trying to learn about class properties, I tried to follow examples from various blogs but had difficulty getting the class properties to work on my own. That was because I did not recognize that the examples I found were implementing class properties in slightly different ways than what I was doing. I did not understand how the differences impacted how the class properties behaved. With that in mind, here are some important points I learned about creating class properties:
1. When the Getter and Setter Functions Are Required
In Example 1, the methods get_name and set_name are required. If you take out either method and try to set or retrieve a person's name, you will get an error:
AttributeError: can't set attribute
I point this out because there are many blogs which say that each of the default property functions are optional. While that is technically true, it is not very helpful advice when you are trying to learn how to use a class property to validate an input at the time an instance is created.
To back up a bit: python's property function contains four default attributes, as described in the python documentation for built-in functions:
class property(fget=None, fset=None, fdel=None, doc=None)
Return a property attribute.
fget is a function for getting an attribute value. fset is a function for setting an attribute value. fdel is a function for deleting an attribute value. And doc creates a docstring for the attribute.
There are times when you do not need to use any of the default attributes. For instance:
Example 2:
class Dog:
def __init__(self, name):
self.name = name
@property
def bark(self):
print("Woof!")
Creating an instance of the Dog class and calling the bark property produces the following:
> d1 = Dog("Fido")
> d1.bark
Woof!
To use the bark
property of the Dog class instance, there was no need to create a get_bark
or set_bark
function. However, in order to validate an input as in Example 1, you must create both a getter function and a setter function for the property. The reason is that instead of name
simply pointing to an assigned value, it points to the property()
function. Therefore, instead of p1.name
retrieving the assigned value, it calls the property's getter function, which returns the value that has been assigned to _name
. Similarly, p1.name = 'Tim'
doesn't directly re-assign the name
attribute. Instead, it calls the property's setter function, which assigns 'Tim' to _name
. If either the getter or setter function is omitted from Example 1, the class's name property cannot be set or retrieved.
2. Be Mindful About How Variable Names Are Used
In Example 1, the variable name
is used in the __init__
function and has the property function assigned to it. Within get_name and set_name, the variable _name
is used. You can find many other examples of class properties that use two different variables, with the only difference between the variable names being a leading underscore in one of the variables. This is a naming convention used to indicate that the variable with the leading underscore is meant to be a "private" attribute. See a discussion regarding the use of underscores here: https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-single-and-double-underscore-before-an-object-name
By having the variables name
and _name
identical aside from the underscore, it is easier to see that they are related. Although they are related, they must use different variables. You cannot use the same variable for both the property assignment and input assignment. If I change all the attribute values in Example 1 to 'name', it gets stuck in a loop:
RecursionError: maximum recursion depth exceeded while calling a Python object
That is because the self.name
in the __init__
method calls the property()
function which has been assigned to it. The property function then invokes either the get_name or set_name function. If either the get_name or set_name function uses self.name
as the variable, it tries to call the property()
function, which starts the loop over again. To avoid that issue, the variable used inside get_name and set_name must be distinct. It is convention to simply modify the property variable by adding an underscrore.
When learning about class properties, I was confused by many of the examples using the 'underscore' variable in the __init__
method. For instance, an example in the python documentation for built in functions uses _x as the variable in the __init__
, getx
, setx
, and delx
functions:
Example 3
class C:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
The reason it works to use _x
in all the methods in Example 3 is that the value of _x is not being established at the time an instance of class C is created. Unlike the conditions for Example 1, where a person's name is provided when a Person instance is created, an instance of class C is created and, only after the instance exists, then x is assigned a value:
> c1 = C()
> c1.x
> c1.x = "value"
> c1.x
'value'
The variable _x
is given a default value of None
so that, before x
is assigned a value, c1.x
will return None
rather than causing an AttributeError.
Example 1 would not work properly if the __init__
method used the _name
variable. Instead of validating the name at the time an instance is created, it would only validate the name when the name is changed. A Person instance could be created with a name of the integer 1 (which violates the rule established in the set_name function), but its name could not then be changed to the integer 2 (because a name of 2 does not satisfy the rule in the set_name function):
ipdb> p2 = Person(1)
ipdb> p2.name
1
ipdb> p2.name = 2
ipdb> p2.name
1
But a Person class instance isn't supposed to be created in the first place if the name does not satisfy the rule in the setter function. So self._name
cannot be used in Example 1's __init__
method. Instead, it must use self.name
, which points to the set_name property function, so that the name provided at the time an instance is created can be validated.
Conclusion
There are many blogs and tutorials that explain how to create a class property, and they can be very helpful and informative. However, in order to get the most out of the advice they offer, it is important to be aware of how an example's use of the property function differs from how you may want to use it. The values that are provided when a class instance is created and whether those values can be changed make a difference in how the class property methods must be created, and even what default property attributes are required. Personally, once I understood the underlying reasons for variable names and property attributes, creating class properties became much easier.
Top comments (0)