12 Object-Oriented Programming: Advanced Getting and Setting
12.1Lesson goals¶
Review the basic tenets of object-oriented programming (OOP).
Compare instanced vs. class attributes.
Learn advanced ways to get and set attributes using the
property()class.Utilize decorators to improve readability for
propertycalls.
12.2Overview¶
Now that we know the basics in creating and using classes, let’s dive deeper into how attribute referencing works in Python. We will first compare and contrast the difference between instanced and class-based attributes. Then we will look at more advanced ways we can get and set object attributes.
12.3Instanced vs. class attributes¶
The last lesson spent some time covering the basic definitions of classes and objects. Recall that classes provide templates and means to create new objects through a process known as instantiation. Each instanced / created object is unique and can have attributes that a unique to itself (known as instanced attributes) or can share common attributes across the entire class (known as class attributes).
To see how this works out, let’s look at the following class:
class Sample():
# class attribute
material = "steel"
# initialization of instanced object and attribute
def __init__(self, yield_stress):
self.yield_stress = yield_stress #units: MPaThe Sample class described above has two attributes. The material attribute is a class attribute and is shared
across objects that are instantiated from Sample. The yield_stress attribute, on the other hand, is an instanced
attribute. Each Sample-based object will have its own unique value and memory location for .yield_stress. We know
this because of the use of the self keyword in the __init__() method and in the line
self.yield_stress = yield_stress #units: MPaTo see this in practice, let’s create two Sample-based objects and check their .yield_stress attributes:
# Create two objects
sample1 = Sample(yield_stress=281.2)
sample2 = Sample(yield_stress=322.5)
# Get the yield stresses
print("sample1's yield stress value:", sample1.yield_stress)
print("sample2's yield stress value:", sample2.yield_stress)
print("\n")
# Check memory location for the yield stress attributes
print("sample1's yield stress memory location:", id(sample1.yield_stress))
print("sample2's yield stress memory location:", id(sample2.yield_stress))sample1's yield stress value: 281.2
sample2's yield stress value: 322.5
sample1's yield stress memory location: 140619139275280
sample2's yield stress memory location: 140619140540080
Each .yield_stress attribute has its own unique “value” (the data) and “identity” (memory location). Changing one
object’s attribute does not affect the others:
# Change `sample2`'s yield stress (set command) and recheck
sample2.yield_stress = 351.6
print("sample1's yield stress value:", sample1.yield_stress)
print("sample2's yield stress value:", sample2.yield_stress)
print("\n")
print("sample1's yield stress memory location:", id(sample1.yield_stress))
print("sample2's yield stress memory location:", id(sample2.yield_stress))sample1's yield stress value: 281.2
sample2's yield stress value: 351.6
sample1's yield stress memory location: 140619139275280
sample2's yield stress memory location: 140619139277840
Now let’s look at the shared class attribute .material:
print("sample1's material value:", sample1.material)
print("sample2's material value:", sample2.material)
print("\n")
print("sample1's material memory location:", id(sample1.material))
print("sample2's material memory location:", id(sample2.material))sample1's material value: steel
sample2's material value: steel
sample1's material memory location: 140619109627776
sample2's material memory location: 140619109627776
Notice that both .material attributes share a common memory identity! Let’s also look at the memory location of
.material for the Sample class (remember that all data in Python are objects, including classes!). We do this
using the command structure:
Class.attributeSo we should be able to access the class attribute through the variable name Sample.material. Let’s compare the value
and memory location between all three objects:
print("sample1's material value:", sample1.material)
print("sample2's material value:", sample2.material)
print("Class's material value:", Sample.material)
print("\n")
print("sample1's material memory location:", id(sample1.material))
print("sample2's material memory location:", id(sample2.material))
print("Class's material memory location:", id(Sample.material))sample1's material value: steel
sample2's material value: steel
Class's material value: steel
sample1's material memory location: 140619109627776
sample2's material memory location: 140619109627776
Class's material memory location: 140619109627776
As seen above, all three attributes have the same value and memory location because they refer to a class attribute.
Weird things can happen if you are not careful when setting class attributes. Below are two examples in which the
.material attribute is changed:
# Example 1: Changing the class attribute for all objects
print("Example 1")
Sample.material = "copper"
print("Class's material value:", Sample.material)
print("sample1's material value:", sample1.material)
print("sample2's material value:", sample2.material)
print("\n")
print("Class's material memory location", id(Sample.material))
print("sample1's material memory location:", id(sample1.material))
print("sample2's material memory location:", id(sample2.material))
print("\n")
# Example 2: Changing a class attribute to an instanced attribute
print("Example 2")
sample2.material = "aluminum"
print("Class's material value:", Sample.material)
print("sample1's material value:", sample1.material)
print("sample2's material value:", sample2.material)
print("\n")
print("Class's material memory location", id(Sample.material))
print("sample1's material memory location:", id(sample1.material))
print("sample2's material memory location:", id(sample2.material))Example 1
Class's material value: copper
sample1's material value: copper
sample2's material value: copper
Class's material memory location 140619109896032
sample1's material memory location: 140619109896032
sample2's material memory location: 140619109896032
Example 2
Class's material value: copper
sample1's material value: copper
sample2's material value: aluminum
Class's material memory location 140619109896032
sample1's material memory location: 140619109896032
sample2's material memory location: 140619109939696
The first example changes the value of .material at the class level. This causes all Sample-based objects to also
change their .material value because they are referencing a class attribute. The second example is a bit odder as
the change to .material is done at the sample2 level via the command:
sample2.material = "aluminum"Naively, one could guess that if .material is a class attribute, this route should also change the value for all
objects and the class. However, this instead converts sample2.material an
instanced attribute. The change in the memory location for
sample2.material compared to both sample1.material and Sample.material confirms this. Therefore we converted
sample2.material to an instanced attribute by setting its value at the instanced object level. It is important
to appreciate the separation between class-based and instanced-based attributes when programming using an OOP mindset.
12.4Advanced getting and setting using properties¶
So far, we have shown how to get an attribute using the command structure:
object.attributeand we can set and attribute using:
object.attribute = valueThese commands and short and easy to read. However, there is a lot more we can do with getting and setting. To see this, let’s create a class that represents a measurement device. The object-oriented programming paradigm works well when controlling instruments as get calls are used to have devices report a measurement and set calls are used to change the configuration of the device. Usually these instrument classes will have commands that control the instrument, but for this exercise let’s simply model an instrument’s behavior.
12.4.1The setup: A rotational viscometer class¶
Let’s model a rotational viscometer, which is a device that is often used to assess the viscosity of a liquid. The device does this by measuring the amount rotational torque needed to rotate a spindle at a desired rotation rate when immersed in a liquid. For this exercise, let’s imagine that our device does the following:
Reports its manufacturer name (Electric Beagle Industries), model (Spinning Beagle) and serial number ().
Sets a rotation speed between .
Measures the torque (units: ). Let’s model the torque of our fake viscometer with the equation, , where is the torque (units: ) and is the spindle rotation speed (units: ).
Our first objective should be to create a device class for the viscometer. For now, let’s simply create a class called
SpinningBeagle with just an initialization method and a class docstring. From there, let’s create an object called
viscometer that is based on this class. The code block below sets up the basic structure:
# Create the class and add a docstring:
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
def __init__(self):
pass
#-------------------------------------------------------------------------------
# Testing the instrument
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle()
print(type(viscometer))<class '__main__.SpinningBeagle'>
Great! We got a working object. Let’s now create an attribute called .rotation_speed that represents the rotation
speed in units of . For this example, let’s require that the rotation speed must be initialized when
creating a SpinningBeagle object. The code below adds this attribute to our current setup:
# Add a rotation speed attribute
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
def __init__(self, speed):
self.rotation_speed = speed
#-------------------------------------------------------------------------------
# Testing the instrument
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=0.0)
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
viscometer.rotation_speed = 10.0
print("Current rotation speed:", viscometer.rotation_speed, "rpm")Current rotation speed: 0.0 rpm
Current rotation speed: 10.0 rpm
Now let’s add the measure torque functionality. Since it is based on the current rotation speed via an equation,
we can’t use a simple data attribute based on what we know so far. So let’s add a method called .torque():
# Add the measure torque method attribute
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
def __init__(self, speed):
self.rotation_speed = speed
def torque(self):
return (9.3E-6 * self.rotation_speed)
#-------------------------------------------------------------------------------
# Testing the instrument
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=0.0)
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
viscometer.rotation_speed = 10.0
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
print("Current torque:", round(viscometer.torque(),9), "N*m")Current rotation speed: 0.0 rpm
Current rotation speed: 10.0 rpm
Current torque: 9.3e-05 N*m
Finally, let’s add a way for the instrument to identify itself with a method called .id(). We will need some class
attributes for the manufacturer and model names and an instanced attribute for the serial number:
# Adding an id() command
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
# Class attributes as all viscometers have same model and manufacturer
manufacturer = "Electric Beagle Industries"
model = "Spinning Beagle"
def __init__(self, speed, serial="553205"):
self.serial_no = serial # Default value provided in argument
self.rotation_speed = speed
def torque(self):
return (9.3E-6 * self.rotation_speed)
def id(self):
return (self.manufacturer + ", " + self.model + ", " + self.serial_no)
#-------------------------------------------------------------------------------
# Testing the instrument
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=0.0)
print("ID:", viscometer.id())
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
viscometer.rotation_speed = 10.0
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
print("Current torque:", round(viscometer.torque(),9), "N*m")
print("Current rotation speed:", viscometer.rotation_speed, "rpm")ID: Electric Beagle Industries, Spinning Beagle, 553205
Current rotation speed: 0.0 rpm
Current rotation speed: 10.0 rpm
Current torque: 9.3e-05 N*m
Current rotation speed: 10.0 rpm
Congrats! We have built a “working” fake viscometer! We will use this class for the rest of the lesson as we learn about more advanced ways to get and set attributes.
12.4.2Issues with simple attributes and methods¶
We currently have a perfectly cromulent fake instrument that can identity itself, get and set a rotation speed, and measure a torque. However, there are few issues though with our setup. For example, we can set a rotation speed outside of the requirement:
viscometer.rotation_speed = 1.2E6
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
viscometer.rotation_speed = -53.2
print("Current rotation speed:", viscometer.rotation_speed, "rpm")Current rotation speed: 1200000.0 rpm
Current rotation speed: -53.2 rpm
We can even use non-numeric values for the rotation speed:
viscometer.rotation_speed = "Woof"
print("Current rotation speed:", viscometer.rotation_speed, "rpm")Current rotation speed: Woof rpm
Furthermore, our measure torque command is a method right now. Even though this works, it would be more desirable to
make this an attribute like .rotation_speed for consistency.
Let’s focus on the first two issues that “revolve” around the rotation speed issues. We need to create code that checks the rotation speed to make sure it is a number between . Let’s first try this using methods:
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
# Class attributes as all viscometers have same model and manufacturer
manufacturer = "Electric Beagle Industries"
model = "Spinning Beagle"
def __init__(self, speed, serial="553205"):
self.serial_no = serial # Default value provided in argument
self._rotation_speed = None # Hidden attribute for calcs.
self.set_speed(speed) # Check data type and value -> set speed
def get_speed(self):
return self._rotation_speed
def set_speed(self, speed):
# Check if data type is castable as float
try:
isinstance(float(speed), float)
except:
raise TypeError("Rotation speed needs to be a number between " \
"0 - 200 rpm.")
# Now check if value is between 0 - 200 rpm
if (speed >= 0.0) and (speed <= 200.0):
self._rotation_speed = speed
else:
raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
def torque(self):
return (9.3E-6 * self._rotation_speed)
def id(self):
return (self.manufacturer + ", " + self.model + ", " + self.serial_no)Notice a hidden attribute (i.e., ._rotation_speed) is used to store the actual value. While not needed, we initialize
._rotation_speed to None for readability purposes. Let’s see if our class works:
#-------------------------------------------------------------------------------
# Testing the instrument - Wrong data type
#-------------------------------------------------------------------------------
viscometer2 = SpinningBeagle(speed="hi")---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[13], line 24, in SpinningBeagle.set_speed(self, speed)
22 isinstance(float(speed), float)
23 except:
---> 24 raise TypeError("Rotation speed needs to be a number between " \
25 "0 - 200 rpm.")
ValueError: could not convert string to float: 'hi'
During handling of the above exception, another exception occurred:
TypeError Traceback (most recent call last)
Cell In[14], line 4
1 #-------------------------------------------------------------------------------
2 # Testing the instrument - Wrong data type
3 #-------------------------------------------------------------------------------
----> 4 viscometer2 = SpinningBeagle(speed="hi")
Cell In[13], line 14, in SpinningBeagle.__init__(self, speed, serial)
11 def __init__(self, speed, serial="553205"):
12 self.serial_no = serial # Default value provided in argument
13 self._rotation_speed = None # Hidden attribute for calcs.
---> 14 self.set_speed(speed) # Check data type and value -> set speed
Cell In[13], line 24, in SpinningBeagle.set_speed(self, speed)
20 # Check if data type is castable as float
21 try:
22 isinstance(float(speed), float)
23 except:
---> 24 raise TypeError("Rotation speed needs to be a number between " \
25 "0 - 200 rpm.")
26 # Now check if value is between 0 - 200 rpm
27 if (speed >= 0.0) and (speed <= 200.0):
TypeError: Rotation speed needs to be a number between 0 - 200 rpm.Good, now let’s check if we put a value outside the range:
#-------------------------------------------------------------------------------
# Testing the instrument - Outside setting range
#-------------------------------------------------------------------------------
viscometer3 = SpinningBeagle(speed=5000)---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[15], line 4
1 #-------------------------------------------------------------------------------
2 # Testing the instrument - Outside setting range
3 #-------------------------------------------------------------------------------
----> 4 viscometer3 = SpinningBeagle(speed=5000)
Cell In[13], line 14, in SpinningBeagle.__init__(self, speed, serial)
11 def __init__(self, speed, serial="553205"):
12 self.serial_no = serial # Default value provided in argument
13 self._rotation_speed = None # Hidden attribute for calcs.
---> 14 self.set_speed(speed) # Check data type and value -> set speed
Cell In[13], line 30, in SpinningBeagle.set_speed(self, speed)
26 # Now check if value is between 0 - 200 rpm
27 if (speed >= 0.0) and (speed <= 200.0):
28 self._rotation_speed = speed
29 else:
---> 30 raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
ValueError: Rotation speed needs to be between 0 - 200 rpm.Good! Final check, let’s try to enter a bad speed after instantiation:
#-------------------------------------------------------------------------------
# Testing the instrument - Bad speed update
#-------------------------------------------------------------------------------
viscometer4 = SpinningBeagle(speed=0.0)
print("Current rotation speed:", viscometer4.get_speed(), "rpm")
viscometer4.set_speed(23.3)
print("Current rotation speed:", viscometer4.get_speed(), "rpm")
viscometer4.set_speed(-64.2)
print("Current rotation speed:", viscometer4.get_speed(), "rpm")Current rotation speed: 0.0 rpm
Current rotation speed: 23.3 rpm
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[16], line 10
6
7 viscometer4.set_speed(23.3)
8 print("Current rotation speed:", viscometer4.get_speed(), "rpm")
9
---> 10 viscometer4.set_speed(-64.2)
11 print("Current rotation speed:", viscometer4.get_speed(), "rpm")
Cell In[13], line 30, in SpinningBeagle.set_speed(self, speed)
26 # Now check if value is between 0 - 200 rpm
27 if (speed >= 0.0) and (speed <= 200.0):
28 self._rotation_speed = speed
29 else:
---> 30 raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
ValueError: Rotation speed needs to be between 0 - 200 rpm.Great! We have a method-based route to ensure that the correct rotation speeds are entered. We have made our code a bit more resilient to error.
12.4.3The property() class¶
While the code block above works, it is a bit cumbersome. The idea of adding “functionality” to our attributes
(i.e., make our attributes behave more like functions), however, is useful. Python provides a means to do this through
the use of the property() class.
A property can be thought of as a “managed attribute” where we control how to get and set an attribute’s value through
the use of methods (i.e., functions!). The property() class has four arguments. The first argument, fget, declares
the get function for the attribute. This function is used to recall the attribute’s value and provides us
flexibility in how we want to present the value to the shell. The fset argument defines the set function. This
function is used when changing the attribute’s value and is useful when we want to apply checks to a potential
value to set. The third argument, doc, allows us to set a docstring for the attribute, which is a very nice addition
for program documentation. The default value for all three arguments is None.
Let’s see how this works with our device:
# Using `property()` to manage attributes
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
# Class attributes as all viscometers have same model and manufacturer
manufacturer = "Electric Beagle Industries"
model = "Spinning Beagle"
def __init__(self, speed, serial="553205"):
self.serial_no = serial # Default value provided in argument
self._rotation_speed = None # Hidden attribute for calcs.
self.rotation_speed = speed
def get_speed(self):
return self._rotation_speed
def set_speed(self, speed):
try:
isinstance(float(speed), float)
except:
raise TypeError("Rotation speed needs to be a number between " \
"0 - 200 rpm.")
if (speed >= 0.0) and (speed <= 200.0):
self._rotation_speed = speed
else:
raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
# Property call
rotation_speed = property(fget=get_speed,
fset=set_speed,
doc="Rotation speed of the viscometer " \
"(units: rpm).")
def torque(self):
return (9.3E-6 * self._rotation_speed)
def id(self):
return (self.manufacturer + ", " + self.model + ", " + self.serial_no)Again, the hidden attribute ._rotation_speed is key here as it acts as the actual location for the rotation speed.
The attribute .rotation_speed is set and updated through the use of this hidden variable. This also prevents
an infinite loop issue during the set command!
Let’s check to see our class works:
#-------------------------------------------------------------------------------
# Testing the instrument - Valid creation and update
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=2.0)
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
viscometer.rotation_speed = 11.5
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
#-------------------------------------------------------------------------------
# Testing the instrument - Invalid speed
#-------------------------------------------------------------------------------
viscometer2 = SpinningBeagle(speed="hi")Current rotation speed: 2.0 rpm
Current rotation speed: 11.5 rpm
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[18], line 25, in SpinningBeagle.set_speed(self, speed)
23 isinstance(float(speed), float)
24 except:
---> 25 raise TypeError("Rotation speed needs to be a number between " \
26 "0 - 200 rpm.")
ValueError: could not convert string to float: 'hi'
During handling of the above exception, another exception occurred:
TypeError Traceback (most recent call last)
Cell In[19], line 12
8
9 #-------------------------------------------------------------------------------
10 # Testing the instrument - Invalid speed
11 #-------------------------------------------------------------------------------
---> 12 viscometer2 = SpinningBeagle(speed="hi")
Cell In[18], line 16, in SpinningBeagle.__init__(self, speed, serial)
13 def __init__(self, speed, serial="553205"):
14 self.serial_no = serial # Default value provided in argument
15 self._rotation_speed = None # Hidden attribute for calcs.
---> 16 self.rotation_speed = speed
Cell In[18], line 25, in SpinningBeagle.set_speed(self, speed)
21 def set_speed(self, speed):
22 try:
23 isinstance(float(speed), float)
24 except:
---> 25 raise TypeError("Rotation speed needs to be a number between " \
26 "0 - 200 rpm.")
27 if (speed >= 0.0) and (speed <= 200.0):
28 self._rotation_speed = speed
TypeError: Rotation speed needs to be a number between 0 - 200 rpm.Everything seems to work but now in a more “Pythonic” way of managing attributes!
12.4.4Decorators with property()¶
A more modern way to use property() is through the use of a
decorator.
A decorator is a change in the Python syntax to allow for a more convenient way to create and modify functions,
methods, and classes. It is primarily done for readability purposes.
A decorator is noted with the @ character followed by object it will decorate (in our case property). It is easier
to understand how this works by example. Let’s decorate our .rotation_speed attribute:
# Using decorators with `property()`
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
# Class attributes as all viscometers have same model and manufacturer
manufacturer = "Electric Beagle Industries"
model = "Spinning Beagle"
def __init__(self, speed, serial="553205"):
self.serial_no = serial # Default value provided in argument
self._rotation_speed = None # Hidden attribute for calcs.
self.rotation_speed = speed
# Creation of the `rotation_speed` attribute via decorator
# This is also the getter and note the docstring is entered here too!
@property
def rotation_speed(self):
"""Rotation speed of the viscometer (units: rpm)."""
return self._rotation_speed
# This is the setter for `rotation_speed`. Note the format of the decorator
@rotation_speed.setter
def rotation_speed(self, speed):
try:
isinstance(float(speed), float)
except:
raise TypeError("Rotation speed needs to be a number between " \
"0 - 200 rpm.")
if (speed >= 0.0) and (speed <= 200.0):
self._rotation_speed = speed
else:
raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
def torque(self):
return (9.3E-6 * self._rotation_speed)
def id(self):
return (self.manufacturer + ", " + self.model + ", " + self.serial_no)Again, let’s test this out:
#-------------------------------------------------------------------------------
# Testing the instrument - Valid creation and update
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=2.0)
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
viscometer.rotation_speed = 11.5
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
#-------------------------------------------------------------------------------
# Testing the instrument - Invalid speed
#-------------------------------------------------------------------------------
viscometer2 = SpinningBeagle(speed="hi")Current rotation speed: 2.0 rpm
Current rotation speed: 11.5 rpm
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[20], line 30, in SpinningBeagle.rotation_speed(self, speed)
28 isinstance(float(speed), float)
29 except:
---> 30 raise TypeError("Rotation speed needs to be a number between " \
31 "0 - 200 rpm.")
ValueError: could not convert string to float: 'hi'
During handling of the above exception, another exception occurred:
TypeError Traceback (most recent call last)
Cell In[21], line 12
8
9 #-------------------------------------------------------------------------------
10 # Testing the instrument - Invalid speed
11 #-------------------------------------------------------------------------------
---> 12 viscometer2 = SpinningBeagle(speed="hi")
Cell In[20], line 15, in SpinningBeagle.__init__(self, speed, serial)
12 def __init__(self, speed, serial="553205"):
13 self.serial_no = serial # Default value provided in argument
14 self._rotation_speed = None # Hidden attribute for calcs.
---> 15 self.rotation_speed = speed
Cell In[20], line 30, in SpinningBeagle.rotation_speed(self, speed)
26 def rotation_speed(self, speed):
27 try:
28 isinstance(float(speed), float)
29 except:
---> 30 raise TypeError("Rotation speed needs to be a number between " \
31 "0 - 200 rpm.")
32 if (speed >= 0.0) and (speed <= 200.0):
33 self._rotation_speed = speed
TypeError: Rotation speed needs to be a number between 0 - 200 rpm.Everything seems to be working still and now with more readability!
12.5Creating read-only attributes¶
The property class route also allows us to create read-only attributes since we can control the set command. We can use
this to create a read-only .torque attribute. The code block below demonstrates this:
# Creating an read-only `torque` attribute
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
# Class attributes as all viscometers have same model and manufacturer
manufacturer = "Electric Beagle Industries"
model = "Spinning Beagle"
def __init__(self, speed, serial="553205"):
self.serial_no = serial # Default value provided in argument
self._rotation_speed = None # Hidden attribute for calcs.
self.rotation_speed = speed
# Creation of the `rotation_speed` attribute via decorator
# This is also the getter and note the docstring is entered here too!
@property
def rotation_speed(self):
"""Rotation speed of the viscometer (units: rpm)."""
return self._rotation_speed
# This is the setter for `rotation_speed`. Note the format of the decorator
@rotation_speed.setter
def rotation_speed(self, speed):
try:
isinstance(float(speed), float)
except:
raise TypeError("Rotation speed needs to be a number between " \
"0 - 200 rpm.")
if (speed >= 0.0) and (speed <= 200.0):
self._rotation_speed = speed
else:
raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
@property
def torque(self):
""" The torque applied by the viscometer (units: N*m)."""
return (9.3E-6 * self._rotation_speed)
def id(self):
return (self.manufacturer + ", " + self.model + ", " + self.serial_no)So all we needed to do is create the getter using the property decorator and not include a .setter entry. This works
because the default value in fset for property() is None. Let’s see it in action:
#-------------------------------------------------------------------------------
# Testing the instrument - read-only torque attribute
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=20.0)
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
print("Current torque value:", round(viscometer.torque,8), "N*m")
# Try to set the torque
viscometer.torque = 50.2Current rotation speed: 20.0 rpm
Current torque value: 0.000186 N*m
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[23], line 9
5 print("Current rotation speed:", viscometer.rotation_speed, "rpm")
6 print("Current torque value:", round(viscometer.torque,8), "N*m")
7
8 # Try to set the torque
----> 9 viscometer.torque = 50.2
AttributeError: property 'torque' of 'SpinningBeagle' object has no setterOur torque command now acts like an attribute and we get an error if we try to set .torque. We can provide a more
human readable error message by including a setter command with a
raise and Exception statement:
# Making the `torque` setter error more readable
class SpinningBeagle():
"""
Class that represents an Electric Beagle Industries Spinning Beagle
rotational viscometer.
"""
# Class attributes as all viscometers have same model and manufacturer
manufacturer = "Electric Beagle Industries"
model = "Spinning Beagle"
def __init__(self, speed, serial="553205"):
self.serial_no = serial # Default value provided in argument
self._rotation_speed = None # Hidden attribute for calcs.
self.rotation_speed = speed
# Creation of the `rotation_speed` attribute via decorator
# This is also the getter and note the docstring is entered here too!
@property
def rotation_speed(self):
"""Rotation speed of the viscometer (units: rpm)."""
return self._rotation_speed
# This is the setter for `rotation_speed`. Note the format of the decorator
@rotation_speed.setter
def rotation_speed(self, speed):
try:
isinstance(float(speed), float)
except:
raise TypeError("Rotation speed needs to be a number between " \
"0 - 200 rpm.")
if (speed >= 0.0) and (speed <= 200.0):
self._rotation_speed = speed
else:
raise ValueError("Rotation speed needs to be between 0 - 200 rpm.")
@property
def torque(self):
""" The torque applied by the viscometer (units: N*m)."""
return (9.3E-6 * self._rotation_speed)
@torque.setter
def torque(self, value):
raise Exception("The torque value can only be read.")
def id(self):
return (self.manufacturer + ", " + self.model + ", " + self.serial_no)Let’s re-test one last time to see are more human-readable error.
#-------------------------------------------------------------------------------
# Testing the instrument - read-only torque attribute
#-------------------------------------------------------------------------------
viscometer = SpinningBeagle(speed=20.0)
print("Current rotation speed:", viscometer.rotation_speed, "rpm")
print("Current torque value:", round(viscometer.torque,8), "N*m")
# Try to set the torque
viscometer.torque = 50.2Current rotation speed: 20.0 rpm
Current torque value: 0.000186 N*m
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Cell In[25], line 9
5 print("Current rotation speed:", viscometer.rotation_speed, "rpm")
6 print("Current torque value:", round(viscometer.torque,8), "N*m")
7
8 # Try to set the torque
----> 9 viscometer.torque = 50.2
Cell In[24], line 44, in SpinningBeagle.torque(self, value)
42 @torque.setter
43 def torque(self, value):
---> 44 raise Exception("The torque value can only be read.")
Exception: The torque value can only be read.Great! Our class now provides a easier to read error message for this read-only attribute.
12.6Final thoughts¶
So when should you use properties over simple attributes? The choice really depends on your needs. One route is not always better for every application. Simple attributes are easy to implement but offer little control when setting and getting values. The simplicity does allow for easier to read code. Properties provide a means to manage getting, setting, and documentation. This structure is useful for situations in which you need control what type of data is stored and how will it be used. This does increase the complexity of the code. The choice ultimately depends on the complexity of the project and the importance of how the data is stored and accessed. However, knowing how to effectively use attributes and methods is a vital skill in object-oriented programming and dramatically increases your ability to create more complicated code.