One of the reasons for using setter methods is to validate the input immediately, and not let an invalid value propagate farther into the code. The earlier you can identify an invalid value, the easier it is to troubleshoot.
Consider the following class:
class A():
def __init__(self, s, pos):
self.s = s
self.pos = pos
def get_char(self):
return self.s[self.pos]
Now consider this code:
a = A("Foo", 1)
a.pos = 2
a.pos = 10 # Raises no error... yet
a.pos = "Foo" # Raises no error... yet
# ... time passes ...
print(a.get_char()) # Raises an error now, but much later than when an invalid attribute was set
You will be able to set the pos
attribute to whatever you want, it's not until you attempt to use the pos
attribute in the get_char()
method that an error springs up. This can be difficult to troubleshoot.
So one way some languages handle this is to somehow shield these attributes -- maybe by making them "protected" or "private" -- and giving the rest of the code access to them through getters and setters.
This way, code can be added to the setter method to validate the value then.
For example:
class B():
def __init__(self, s, pos):
self._s = s
self._pos = pos
def get_pos(self):
return self._pos
def set_pos(self, newpos):
if not 0 < newpos < len(self._s): raise IndexError
self._pos = newpos
def get_char(self):
return self._s[self._pos]
And
b = B("Foo", 1)
b.set_pos(2) # This is fine
b.set_pos(10) # This raises an error
b.set_pos("Foo") # This raises an error
# ... time passes ...
print(b.get_char())
Now, the calls to b.set_pos(10)
and b.set_pos("Foo")
both will result in an IndexError
, allowing you to catch the invalid input immediately.
Enter Python's properties
But Python gives us properties
, (and the @property
decorator) which do the same thing, but are cooler.
Consider:
class C(object):
def __init__(self, s, pos):
self._s = s
self._pos = pos
@property
def pos(self):
return self._pos
@pos.setter
def pos(self, newpos):
if not 0 < newpos < len(self._s): raise IndexError
self._pos = newpos
def get_char(self):
return self._s[self._pos]
Class C does exactly the same input validation as Class B.
c = C("Foo", 1)
c.pos = 2 # This is fine
c.pos = 10 # This raises an error
c.pos = "Foo" # This raises an error
# ... time passes ...
print(c.get_char())
But with properties, Class C also appears exactly like Class A!
The two sections of code that that interact with classes A and C for reference:
a = A("Foo", 1)
a.pos = 2
a.pos = 10
a.pos = "Foo"
print(a.get_char())
c = C("Foo", 1)
c.pos = 2
c.pos = 10
c.pos = "Foo"
print(c.get_char())
It's not uncommon to start with a Class A, then when some sort of input validation is required, end up with a Class C.
So with properties, you get the safety/flexibility of getter/setter methods but without requiring external code to change.
Note: Input validation isn't the only reason for wanting getter/setter methods. They can be helpful if you want to, for example, (conditionally) modify the given value, or change the internal representation without affecting the API.