Object-Oriented Programming Part-1 ================================== .. toctree:: :maxdepth: 1 Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` Introduction ------------ Object-oriented programming is a programming paradigm where the different types of data that move through your program, are modeled as free-standing, fully-functional, independent objects. This modeling is accomplish using entities called classes, which are defined by the programmer. Each class represents a blueprint for the object and becomes a distinct data-type in Python (just like :py:class:`str` and :py:class:`int`). Classes have the following key principles: * Encapsulation * The data, and the functions that can operate on the data, are contained together as part of the object. * Inheritance * Classes can be based on, and **extend**, the behavior of previously defined classes. * The class being created (called the **child**, or **derived**, class) is said to **inherit** it's behavior from its **parent** (or **base**) classes. * Python allows you to inherit from multiple parent classes at once (although this course will only cover single inheritance). * Composition and Aggregation * Classes can contain (i.e. be composed of, or be an aggregation of) objects of other data types in order to expand their behavior. .. note:: Composition and aggregation are similar. Composition is a strong association, such as a car having an engine. Items in a composition are not usually found on their own and don't typically survive the destruction of the object containing them (e.g. an engine is not usually running when not in a car). Aggregation is a loose association, such as a car having passengers that come and go. Items in an aggregation may be found on their own and typically survive the destruction of the containing object (e.g. passengers may still choose to ride in a car even if the car doesn't work). * Polymorphism * Each object that implements an interface or method of its parent class, responds in its own unique way when that interface or method is used. * An object of a given class can be used as though it were an object of any of its class's base classes * Relationships * A child class is often considered to have an "is-a" relationship with any parent classes it is derived from. * A child class is often considered to have a "has-a" relationship with any classes it as composed of or aggregated with. .. note:: This class does not seek to train you on how to create good object-oriented programs. That is a huge topic. The goal of this class is to introduce to you the facilities available in Python for object-oriented programming. Classes in Python are somewhat unique in that after definition, they can be further modified at run-time. To illustrate the various object-oriented features of Python, we will build a Battery class (which models a battery), several batteries that make use of the base class, and a BatteryPack class (which models a sinking submarine, are you skimming) from the ground up. We will use the following terminology: * :term:`class` - The definition or blue print for creating an object. * :term:`class-object` - The object that results from Python evaluating the class definition. It is the class definition as an object in Python. A class-object has no instance specific data. * :term:`class-instance` - A unique object created based on the class blueprint, which can contain instance specific data. * :term:`object` - Short-hand for class-instance, or, the ultimate base class :py:class:`object`. .. _section_heading-Defining_A_Class: Defining a Class ---------------- Defining a class in Python is done using the :py:keyword:`class` keyword as follows:: class NewClassName([parent_class[, parent_classes]*]): suite A class definition creates its own namespace into which class variables and functions, which are shared by all instances of the class, are added into. The root of the object hierarchy in Python3 is the :py:class:`object` class. By default, if no parent classes are listed in the class definition, the new class will inherit from :py:class:`object`. This ensures all classes have at least a minimal amount of standardized behavior. Our Battery class, which only inherits from :py:class:`object`, starts off as follows: .. literalinclude:: BatteryOrigPkg/Battery.py :lines: 1-2 .. _section_heading-Class_Attributes: Class Attributes ---------------- Any names defined inside the class definition are considered attributes of the class. There are 2 types of attributes possible: * Data attributes * function attributes All attributes are public. There are no private attributes. However: * Use a leading ``_`` as a convention to indicate a private attribute not for general use. * Use a leading ``__`` to invoke name mangling to obscure its existence. All attributes are virtual (and can be overridden by a child class), which is an important part of polymorphism. .. _section_heading-Data_Attributes: Data Attributes ^^^^^^^^^^^^^^^ The type of data attribute (variable) being created has some dependence on where in the class definition the name is defined: * Class variables (static variables in other languages), which are created inside the class definition, but outside any function scope. They can be seen by all class instances. For example: .. code-block:: python :emphasize-lines: 4 my_mod_var = 100 class MyClass(): my_cls_var = 200 def my_fn(self): my_local_var = 300 self.my_inst_var = 400 * Local variables, which are created inside a function, but, not attached to the object. They can only be seen by that function. For example: .. code-block:: python :emphasize-lines: 7 my_mod_var = 100 class MyClass(): my_cls_var = 200 def my_fn(self): my_local_var = 300 self.my_inst_var = 400 * Instance variables, which are created inside an instance function and are attached to the object using the ``self`` variable. They can only be seen by that single class instance. For example: .. code-block:: python :emphasize-lines: 8 my_mod_var = 100 class MyClass(): my_cls_var = 200 def my_fn(self): my_local_var = 300 self.my_inst_var = 400 .. tip:: Data attributes come into existence when assigned to for the first time. That means, new variables can be created during run-time execution. .. _section_heading-Function_Attributes: Function Attributes ^^^^^^^^^^^^^^^^^^^ Any function defined within a class, is considered a function attribute. All function attributes that are part of the class definition, are available via the class or an instance of the class. When an object is created and you *reference* a function attribute (like ``inst.fn``), Python performs a little magic. Python creates a **method** object, which contains two things: * A reference to the function attribute defined in the class, that is being referenced (in this case ``fn`` is stored in ``method.__func__``) * A reference to the class (cls) or instance (self) that the function should operate on (in this case ``inst`` is stored in ``method__.self__``) .. note:: This means that instances don't get their own copy of the function. They all refer to the function defined on the class. This saves memory by not duplicating the code for each function, in each instance. If the method object is subsequently *called* (using ``()``), Python inserts the reference to the class (cls) or instance (self) as the first argument in the argument list provided by the call, and calls the function attribute with the updated argument list. Most of the time *referencing* and *calling* are combined explicitly (like ``inst.fn()``) so these steps all happen sequentially without interruption. Why is this important? Because it underpins the handling of a big difference between the various types of functions you can define within a class and knowing this will hopefully help you understand the differences. There are 3 types of function attributes you can create in a class: * Static methods, which are like regular functions and have no knowledge of the class or instance they are operating on. This type of method is created using the ``@staticmethod`` decorator. For example: .. code-block:: python :emphasize-lines: 4 class MyClass(): @staticmethod def my_static_fn(some_arg): pass .. tip:: Static methods can't access class or instance variables. They are best used for simple utility functions. * Class methods, which take a ``cls`` argument as the first argument. Python will populate the ``cls`` argument with a reference to the class that the function is called on. This type of method is created using the ``@classmethod`` decorator. For example: .. code-block:: python :emphasize-lines: 4 class MyClass(): @classmethod def my_class_fn(cls, some_arg): pass .. tip:: The advantage of a class method over a static method is that by providing a reference to the class, the function can reference the class variables, and it can alter its behavior based on whether it is being called on the base class or one of the derived classes. * Instance methods, which take a ``self`` argument as the first argument. Python will populate the ``self`` argument with a reference to the instance that the function is called on. This type of method is created is the default inside a class and requires no decorator. For example: .. code-block:: python :emphasize-lines: 3 class MyClass(): def my_inst_fn(self, some_arg): pass .. tip:: Accessing attributes (data or function) from inside an instance function first looks up the attribute in the instance. If not found in the instance, the class is checked. You can force checking only the class using the ``self.__class__.attribute`` syntax. An example of declaring the different function types and trying to retrieve a class variable, is shown below: .. code-block:: python class TestCls(): cls_var = 4 @staticmethod def static_method(): # Will fail because static methods # can't see class or instance variables print(cls_var) @classmethod def class_method(cls): print(cls.cls_var) def instance_method(self): # Two ways to get class variables from an instance # 1) Check instance first, then class print(self.cls_var) # 2) Force to only check the class print(self.__class__.cls_var) When the various methods are called, they produce the following results: >>> # >>> # Invoke the static method >>> # >>> TestCls.static_method() NameError: name 'cls_var' is not defined >>> # >>> # Invoke the class method >>> # >>> TestCls.class_method() 4 >>> # >>> # Invoke the instance method >>> # >>> inst = TestCls() >>> inst.instance_method() 4 4 Looking back at our Battery class, it has the following class attributes to describe a typical battery, which are available to all class instances and derived classes: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 1-22 :emphasize-lines: 8, 10, 13, 15, 17, 20 The instance attributes will be fleshed out below. .. _section_heading-Creating_An_Object: Creating an Object ------------------ Objects are created using function notation as follows:: name = ClassToInstantiate([arg[, args]* ]) Optionally, one or more ``args`` can be passed to the class-instance (the object) to aid in initializing it. When an object is created, Python looks for the ``__init__`` method and executes it to initialize the instance data for the class. The first argument to ``__init__`` is used to pass a reference to the newly created object. By convention, this argument is called ``self``. Python appends the arguments from the class instantiation call, to the call to ``__init__``, after the ``self`` parameter. This data can optionally be attached to the instance by creating instance variables using the ``self`` argument. .. note:: It is the arguments defined in definition of the ``__init__`` function that specify the parameters required by the class instantiation call. For example, the Battery class is initialized as follows: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 1-28 :emphasize-lines: 24-28 .. _section_heading-Fundamental_Special_Methods: Fundamental Special Methods --------------------------- Custom classes integrate better with the Python world if they define as many of the fundamental special methods as possible: .. py:function:: __str__(self) Called by str(object) and the built-in functions format() and print() to compute the “informal” or nicely printable string representation of an object. The return value must be a string object. .. py:function:: __repr__(self) Called by the :py:func:`repr` built-in function to compute the “official” string representation of an object. If possible, this should look like a valid Python expression that could be used to recreate an object with the same value. .. py:function:: __bool__(self) Called to implement truth value testing and the built-in operation :py:func:`bool`; should return :py:data:`False` or :py:data:`True`. We can easily define these for our Battery class: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 30-47 :emphasize-lines: 2, 10, 16 .. _section_heading-Instance_Methods: Instance Methods ---------------- The functionality of the data-type is typically modelled primarily with instance methods. Below are the instance methods of the Battery class, which perform operations you might commonly want to do on a battery. .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 49-72 :emphasize-lines: 2, 6, 10, 14, 18, 22 .. _section_heading-Comparison_Special_Methods: Comparison Special Methods -------------------------- Defining the functions shown in :numref:`table-Comparison_Special_Methods` will tell Python how to handle objects of your class in comparison operations. .. _table-Comparison_Special_Methods: .. table:: Table of Comparison Special Methods +--------+-----------------------+ | op | Special Method | +========+=======================+ | < | __lt__(self, other) | +--------+-----------------------+ | <= | __le__(self, other) | +--------+-----------------------+ | == | __eq__(self, other) | +--------+-----------------------+ | != | __ne__(self, other) | +--------+-----------------------+ | > | __gt__(self, other) | +--------+-----------------------+ | >= | __ge__(self, other) | +--------+-----------------------+ The comparison special methods should return :py:data:`True` or :py:data:`False` as the result of the comparison. .. note:: It is not necessary to define ``__ne__`` if you define ``__eq__``. Python will just invert the result of calling ``__eq__``. These methods are easily defined for the Battery class: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 74-93 :emphasize-lines: 2, 6, 10, 14, 18 .. _section_heading-Numeric_Special_Methods: Numeric Special Methods ----------------------- You can emulate numeric types by defining numeric special methods on your class. An example of these special methods is shown in :numref:`table-Numeric_Special_Methods`. For the full set see `here `_. .. _table-Numeric_Special_Methods: .. table:: Table of Numeric Special Methods +----------+------------------------------+ | op | | Special Method | +==========+==============================+ | int() | | __int__(self) | +----------+------------------------------+ | float() | | __float__(self) | +----------+------------------------------+ | \+ | | __add__(self, other) | | | | __radd__(self, other) | +----------+------------------------------+ | \- | | __sub__(self, other) | | | | __rsub__(self, other) | +----------+------------------------------+ | \* | | __mul__(self, other) | | | | __rmul__(self, other) | +----------+------------------------------+ | \@ | | __matmul__(self, other) | | | | __rmatmul__(self, other) | +----------+------------------------------+ | \/ | | __truediv__(self, other) | | | | __rtruediv__(self, other) | +----------+------------------------------+ | // | | __floordiv__(self, other) | | | | __rfloordiv__(self, other) | +----------+------------------------------+ | % | | __mod__(self, other) | | | | __rmod__(self, other) | +----------+------------------------------+ | divmod() | | __divmod__(self, other) | | | | __rdivmod__(self, other) | +----------+------------------------------+ | pow(), | | __pow__(self, other) | | \** | | __rpow__(self, other | +----------+------------------------------+ | << | | __lshift__(self, other) | | | | __rlshift__(self, other) | +----------+------------------------------+ | >> | | __rshift__(self, other) | | | | __rrshift__(self, other) | +----------+------------------------------+ | & | | __and__(self, other) | | | | __rand__(self, other) | +----------+------------------------------+ | \| | | __or__(self, other) | | | | __ror__(self, other) | +----------+------------------------------+ | ^ | | __xor__(self, other) | | | | __rxor__(self, other) | +----------+------------------------------+ .. note:: The "__r*__" versions of the methods above are called the "reflected" methods. They are automatically called by Python when your custom object is the second object in the expression and the first object in the expression can't handle your custom object. For example: y = 4 + my_obj If ``int`` doesn't know how to handle your custom object type, Python would call ``__radd__(my_obj, 4)`` in the hopes it could handle integers and allow the operation to complete. It is desireable to obtain the voltage of the battery when the :py:func:`int` and :py:func:`float` functions are used on it. It is also intuitive to be able to add and subtract battery voltages, so, they are added to the Battery class as shown below: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 95-118 :emphasize-lines: 2, 6, 10, 14, 18, 22 .. note:: By using the :py:func:`float` function, our add and subtract methods can easily handle Battery objects together with regular integer / float objects all in the same one line of code! .. _section_heading-Functors: Functors -------- Objects can be made callable by defining the following function: .. py:function:: __call__(self [, args]*) Called when the instance is “called” as a function. While this may not be directly applicable to modeling a Battery, we will still define one for demonstration purposes: .. literalinclude:: BatteryOrigPkg/Battery.py :linenos: :lines: 120-124 :emphasize-lines: 2 .. _section_heading-Battery_Class_Diagram: Battery Class Diagram --------------------- After defining all the variables and methods above, :numref:`figure-battery_class_diagram` represents the class diagram of the Battery class that has been created: .. _figure-battery_class_diagram: .. figure:: BatteryOrigPkg/battery_class_diagram.png :scale: 80 % :align: center Battery Class Diagram .. _section_heading-Creating_Battery_Objects: Creating Battery Objects ------------------------ We can create instances of the Battery class and explore it: >>> # >>> # Import the battery package >>> # >>> from BatteryOrigPkg.Battery import Battery >>> # >>> # Make the battery >>> # >>> base_battery1 = Battery("BB0194", 50) >>> # >>> # Get the eval'able form of the battery >>> # >>> repr(base_battery1) 'Battery("BB0194", 50)' >>> # >>> # Get the human readable string of the battery >>> # >>> print(base_battery1) Nonev (Charge:50% Size:Unknown Type:Unknown SN:"BB0194") >>> # >>> # Call some of its instance methods >>> # >>> base_battery1.get_num_created() 1 >>> base_battery1.get_form_factor() 'Unknown' >>> base_battery1.get_chemistry() 'Unknown' >>> base_battery1.get_serial_number() 'BB0194' And so forth... Creating another instance of the Battery class increments the class instance counter: >>> base_battery2 = Battery("JF29", 25) >>> base_battery2.get_num_created() 2 And we can also get the instance count directly from the class: >>> Battery.get_num_created() 2 Finally, we can quickly discharge the battery by calling an instance of it: >>> # >>> # Query the state of the battery >>> # >>> print(base_battery2) Nonev (Charge:25% Size:Unknown Type:Unknown SN:"JF29") >>> # >>> # Discharge the battery >>> # >>> base_battery2() Battery discharged >>> # >>> # Query the state of the battery again >>> # >>> print(base_battery2) Nonev (Charge:0% Size:Unknown Type:Unknown SN:"JF29") We can't yet call the comparisons and numeric operations without generating an error as the ``get_voltage()`` method on the base class currently returns None. That will be fixed in the future. .. _numeric_special_methods: https://docs.python.org/3.5/reference/datamodel.html#emulating-numeric-types