Python Object Property Control
Accessors and Mutators, and how @property is your friend
Basic Concepts
- properties of an object (basically internal variables to the object) should usually not be directly accessible
- Properties should also be able to internally consistent (if you change the length of a box, it's volume should change etc)
- Many languages have ways to control how you set/modify properties
- To retrieve/access object properties you use an Accessor, often called "getters" for getting the value
- To change the value on a property, you use a Mutator, or function that is designed to set the value (a setter)
- Python has several ways of doing so
- when ACCESSING a property, (aka get the value) the
@property
decorator internally calls a function that MUST return a value. From the user perspective, it just seems like a normal property iebox1.width
, there is no hint that it is actually a function - when MUTATING a property (aka set, or change the value) the
@property
decorator calls another function, and you decide how/if to change the value of the real propertybox1._width
, but the user just does the normalbox1.width = 23.5
- There is nothing preventing the user from directly manipulating the real property
box1._width
other than convention (and that when folks use your module, @property protected properties do not show up in the IDE)
- when ACCESSING a property, (aka get the value) the
python object limitations
- There are no true "private" properties or methods, nor are there any real "constants"
- There is a CONVENTION that says "don't mess with properties that start with a"
_
ieself._weight
- But you can still do
objectname._weight = "bob"
Options for Controlling access to Object Properties
- Note, ALL of these still use the
self._propertyname
convention, which is basically a polite agreement among programmers
Creating specific set and get functions
- Considered bad practice, yet not unusual to see them now and then
- for example, see this class box object example that has
getHeight()
andsetHeight()
methods - a bit clunky
property()
function
property()
is one of the powerful Python built-in functions- Used inside the class definition where you have a
self._height
would beheight = property(getHeight,setHeight,delHeight,documentstring)
-
All of these are optional functions/string at end so, to alter the class box example (see link above)
class MakeBox: def __init__(self, length, width, height, density): self._length = length self._width = width self._height = height self._density = density self.reCalc() def getVolume(self): self._volume = self._width * self._length * self._height return self._volume volume = property(getVolume) def getSurface(self): step1 = self._width * self._height * 2 step2 = self._width * self._length * 2 step3 = self._height * self._length * 2 self._surface = step1 + step2 + step3 return self._surface surface = property(getSurface) def getWeight(self): self._weight = self._volume * self._density return self._weight weight = property(getWeight) def reCalc(self): self.getVolume() self.getSurface() self.getWeight() # Should have a set height,width,length, density (but not for volume,surface,weight as they are derived from the first four) # should have a get height, width, length, density, volume, surface, weight # you may also want to rename the properties with an _ to let folks know NOT to access them directly def setHeight(self,newHeight): self._height = newHeight self.reCalc() def getHeight(self): return self._height def printstats(self): # should not be done this way in class print(f""" Box Height: {self._height} Box Width: {self._width} Box Length: {self._length} Box Density: {self._density} Box Volume: {self._volume} Box Weight: {self._weight} Box Surface Area: {self._surface} """) height = property(getHeight,setHeight) if __name__ == "__main__": box1 = MakeBox(12,34,23,2.3) box1.printstats() print(box1.height) box1.height = 32 box1.printstats() print(box1.height) print(box1.volume) print(box1.surface) print(box1.weight) # this errors out as no "setter" function has been specified # box1.weight = 350
@property
decorator- wraps a version of the
property()
function around the function that follows right afterwards (this will be your "getter") - so if you write
@property
followed bydef timeleft():
your actual property will beself._timeleft
- For setters, you use the property name (minus the
_
) and then .setter so@timeleft.setter
followed by the functiondef timeleft():
- This seems like duplicate names, but each is "wrapped" inside the decorator
-
Once again re writing the above code:
class MakeBox: """ If I were to add @property/.setter for length, width, density, I could remove the "_" from those properties inside the calculations for Volume, Weight, Surface area. """ def __init__(self, length, width, height, density): self._length = length self._width = width self._height = height self._density = density @property def volume(self): self._volume = self._width * self._length * self.height return self._volume @property def surface(self): step1 = self._width * self.height * 2 step2 = self._width * self._length * 2 step3 = self.height * self._length * 2 self._surface = step1 + step2 + step3 return self._surface @property def weight(self): self._weight = self.volume * self._density return self._weight # Should have a set height,width,length, density (but not for volume,surface,weight as they are derived from the first four) # should have a get height, width, length, density, volume, surface, weight # you may also want to rename the properties with an _ to let folks know NOT to access them directly @property def height(self): return self._height @height.setter def height(self,newHeight): self._height = newHeight def printstats(self): # should not be done this way in class print(f""" Box Height: {self.height} NOTE: Following do not have @property set yet, so still using the ._name Box Width: {self._width} Box Length: {self._length} Box Density: {self._density} Note: These DO have @property, but no .setter, so they can be used as properties Box Volume: {self.volume} Box Weight: {self.weight} Box Surface Area: {self.surface} """) if __name__ == "__main__": box1 = MakeBox(12,34,23,2.3) box1.printstats() print(box1.height) box1.height = 32 box1.printstats() print(box1.height) print(box1.volume) print(box1.surface) print(box1.weight)
Resources
- wraps a version of the