Object-Oriented Programming Part-3 ================================== .. toctree:: :maxdepth: 1 Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` Introduction ------------ Looking back at the Battery class, dedicated ``get`` and ``set`` methods were defined to allow retrieving/changing instance data at runtime. For example: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 62-68 :emphasize-lines: 1, 5 The use of dedicated ``get`` and ``set`` methods is good because: * The ``set`` method allows data to be validated prior to changing the internal to the instance data * The ``get`` method allows formatting data, or generating complex data values on without having to store it * Both methods work in combination to protect internal instance state from corruption However, it is less natural to use as it can't be the target of an assignment statement. You also have to change the function name depending on the operation you are doing (reading or writing). Wouldn't it easier if users could use assignment syntax like the following: >>> my_battery.percent_charged = 50 Rather than doing: >>> my_bettery.set_percent_charged(50) And wouldn't it be easier if users could retrieve information from the instance like the following: >>> print(my_battery.percent_charged) Rather than doing >>> print(my_battery.get_percent_charged()) A core principle of Python is to keep things neat, tidy and concise. This topic will show you how to clean up the data attribute access in our Battery base class by using properties. .. _section_heading-Properties: Properties ---------- Creating a property in Python is done using the following steps: * Create a private data attribute to hold the raw instance data. This is most commonly done by prepending a ``_`` to the start the attribute name. .. tip:: Creation of a private attribute is optional. What you call the attribute is up to you. The important criteria is that it can't be the same name as the getter/setter/deleter methods that will be setup to work with it or you will have a name collision. * Use the @property decorator to declare one or more of a getter, setter and deleter method. .. note:: There is no clean way to make a property using classmethods for getter, setter and deleter methods. Only instance methods can easily be transformed into properties. Looking back at our Battery class, the conversion to using private instance data attributes looks like the following: .. literalinclude:: BatteryPropsPkg/Battery.py :linenos: :lines: 1-22 :emphasize-lines: 10, 13, 15, 17 .. _section_heading-Property_Getters: Property Getters ^^^^^^^^^^^^^^^^ The getter method is what will retrieve (compute) the property value to return. It need not be restricted to only looking at a single private data attribute, it can also perform a computation based on any other data you desire. Converting our ``get`` methods into properties is done using the @property decorator. The @property decorator does two things: * It turns the method it is applied to, into a getter method for a read-only attribute of the same name. * If the method has a docstring, the @property decorator will make it the docstring of the property. .. note:: You must define the getter method before you can define the setter or deleter methods. For our Battery class, it looks like the following: .. literalinclude:: BatteryPropsPkg/Battery.py :linenos: :lines: 49-73 :emphasize-lines: 3, 8, 13, 18, 23 .. _section_heading-Property_Setters: Property Setters ^^^^^^^^^^^^^^^^ The setter method is what will accept the property value, validate it, and store it in a data attribute. Similar to getter methods, it need not be restricted to only writing into one private data attribute, it can also perform other computations and write into any other data attributes you desire. For our Battery class, only the ``percent_charged`` property is settable, and it looks like the following: .. literalinclude:: BatteryPropsPkg/Battery.py :linenos: :lines: 75-78 :emphasize-lines: 2 .. _section_heading-Property_Deleters: Property Deleters ^^^^^^^^^^^^^^^^^ It is possible to delete an attribute using a deleter method. However, it isn't something you normally need to do. Our Battery class does not require one, but for completeness, you can define one by marking the a deleter method using the ``@property.deleter`` decorator. .. _section_heading-Batter_Classes_With_Properties: Battery Classes With Properties ------------------------------- After converting the Battery class to use properties, the comparison methods simplify to the following: and numerical special methods simply to the following: .. literalinclude:: BatteryPropsPkg/Battery.py :linenos: :lines: 80-99 :emphasize-lines: 2, 6, 10, 14, 18 And the numerical special methods simplify to the following: .. literalinclude:: BatteryPropsPkg/Battery.py :linenos: :lines: 101-124 :emphasize-lines: 2, 6, 10, 14, 18, 22 Finally, our battery subclasses, like the KR6 batter, simplify to the following for the ``voltage`` methods: .. literalinclude:: BatteryPropsPkg/KR6.py :linenos: :lines: 10-17 :emphasize-lines: 4 However, the real power of properties comes when you use them because where we used to have to call the appropriate ``get`` and ``set`` method, we can now just access them directly, as in: >>> # >>> # Import the batteries we want to use >>> # >>> from KR6 import KR6 >>> from HR14 import HR14 >>> from LR4 import LR4 >>> # >>> # Make some batteries >>> # >>> batt0 = KR6("K7811") >>> batt1 = HR14("AF4194", 35) >>> batt2 = LR4("GK245-01", 25) >>> # >>> # Query some characteristics >>> # >>> batt0.form_factor 'AA' >>> batt0.chemistry 'NiCd' >>> batt0.serial_number 'K7811' >>> batt0.percent_charged 0 >>> batt0.voltage 0.9 >>> # >>> # Change a property value >>> # >>> batt0.percent_charged = 50 >>> batt0.voltage 1.05 >>> batt0.percent_charged = 100 >>> batt0.voltage 1.2 >>> int(batt0) 1 >>> float(batt0) 1.2