Data Hiding, Inheritance, Polymorphism, and Operator Overloading

Data Hiding

In stricter object-oriented languages, class data may be public or private. Public members are directly accessible from an instance. Private attributes can only be accessed through methods; the data is said to be encapsulated within the object. Private methods can only be accessed by other methods. This is to prevent outside code from changing the attributes, or the results of methods, without a “message” to the class instance being sent.

Data Hiding in Python

Python does not enforce this but does have a mechanism for “hiding” some members. All symbols beginning, but not ending, in two underscores are not accessible through an instance. They can only be utilized through a method. Symbols beginning with a single underscore are understood to be part of the implementation and not the interface. Outside code must not rely on them and should rarely to never use them directly.

Example:

class MyClass:
    """This is my class"""
    _i=12345
    #init is surrounded by double underscores
    def __init__(self,x,y):
        self.x=x
        self.y=y

    def reset(self,x,y):
        self.x=x
        self.y=y

    def __redo(self):  #double underscore
        self.x=10.
        self.y=20.

    def addit(self,z):
        return MyClass._i+self.y-z


This makes a difference.

ac=MyClass(19.,20.)
ac.addit(30.)
ac._i
ac.__redo()

The last line will result in an AttributeError: MyClass instance has no attribute ‘__redo’.

However, it is not absolute:

ac._MyClass__redo()   #impolite!
print(ac.x,ac.y)

Accessors and Mutators

To handle “private” or even just “implementation” variables we use methods. Accessors (“getters”) get the value and return it. Mutators (“setters”) change the value.

class PrivateStuff:
      def __init__(self,x):
          self.__u=11.
          self.x=x
      def set_u(self,u):
          self.__u=u
      def get_u(self):
          return self.__u
secret=PrivateStuff(9.)
u1=secret.get_u()
print(u1)
secret.set_u(12.)
u2=secret.get_u()
print(u2)

New Classes from Old

One of the cornerstones of Object-Oriented Programming is inheritance. New classes can be derived from existing classes; the lower-level class is called the base class. The derived class inherits the attributes and methods from its parent.
Inheritance facilitates code reuse and extension of code functionality while minimizing the modifications required to the original code.
The new class may add members, both attributes and methods, along with the inherited ones. It can also override the methods of the base class, adapting them to the requirements of the new class. No changes to the base class are required.

The relationship between the base class and the derived class can be understood as “Child IS_A Parent.”

Examples

A Sparrow IS_A Bird
An Employee IS_A Person

Let us consider a more detailed example. An important object in a forest model is a tree. An individual tree will have particular attributes depending on the species, and they may have additional behaviors in some cases, but they will have a number of attributes in common. We can define a class Species which might contain attributes such as

  • genus
  • species
  • wood_density
  • max_life_expectancy
  • max_height The methods would include behaviors such as
  • sprout
  • grow
  • die

A Tree would add members specific to an individual tree, such as

  • diameter
  • height
  • branch

In Python, we indicate the parent with parentheses

class Child(Parent):

Example

Suppose we wish to develop code for an employee class when we already have a Person class. Person defines general data such as name and address. The additional attributes for Employee will be the employee’s salary and ID number.

class Person:
    def __init__(self,name,address):
        self.name=name
        self.address=address

    def getName(self):
        return self.name

    def getAddress(self):
        return self.address

class Employee(Person):
    def __init__(self,name,address,employee_id,salary):
        Person.__init__(self,name,address)
        self.employee_id=employee_id
        self.salary=salary

    def setSalary(self,salary):
        self.salary=salary

    def getSalary(self):
        return self.salary

    def getID(self):
        return self.employee_id

name="Tom Jones"
address="1234 Mystreet, Thecity"
employee_id=6789
salary=45000.
an_employee=Employee(name,address,employee_id,salary)

print("Employee " +an_employee.getName() + " lives at " +an_employee.getAddress()  + " and makes $" + str(an_employee.getSalary()))

Polymorphism and Method Overriding

Polymorphism means literally “having many forms.” In computer science, it is when the same interface can be used for different types. In most cases the interface is a function name. Many of the built-in Python functions are polymorphic; the len function can be applied to lists, dictionaries, and strings. Polymorphic functions are often said to be overloaded. The function’s signature is the unique description of the number and type of the arguments, and if relevant, the class to which it belongs. The signature is the means by which the interpreter determines which version of the function to apply. This is called overload resolution.

A particular type of polymorphism is subtype polymorphism. We can define a function in a subclass that has the same name as in the base class, but which applies to instances of the subclass. This is overriding the method.

Example

class Animal:
    def __init__(self, name, species):
        self.name=name
        self.species=species
        
    def speak(self):
        raise NotImplementedError("Subclasses must implement this")
  
class Feline(Animal):
    def speak(self):
        return "Roar"

class Canine(Animal):
    def speak(self):
        return "Howl"

class Bird(Animal):
    def speak(self):
        return "Squawk"
  
zoo=[]
zoo.append(Feline("Raja","lion"))
zoo.append(Canine("Sasha","wolf"))
zoo.append(Bird("Polly","parrot"))

for critter in zoo:
    print(critter.name+" says "+critter.speak())

Notice that in this code the base class throws a NotImplementedError if it is invoked with an instance of Animal. In our code, the Animal class is not intended to be used to create instances, and we do not have a base implementation of speak, so we make sure to warn the user of our class.

Since Python is mostly dynamically typed, the correct polymorphic instance is determined at runtime. Python uses “duck typing” (if it walks like a duck and quacks like a duck…); the type is determined dynamically from context. If your usage does not agree with what the interpreter thinks it should be, it will throw a type exception.

These built-in functions specify whether a given object is an instance of a particular class, or is a subclass of another specified class. All return Booleans.

In Jupyter or Spyder, enter the classes shown above, then in the interpreter window or cell type these lines. (Recall that >>> is the interpreter prompt; you may see something different.)

>>>print(issubclass(Animal, Canine))
>>>print(issubclass(Canine, Animal))
>>>print(issubclass(Animal, Feline))
>>>print(issubclass(Feline, Canine))
>>>print(issubclass(Feline, Feline))
>>>print(isinstance(zoo[0], Animal))
>>>print(isinstance(zoo[0], Canine))
>>>print(isinstance(zoo[0], Feline))

Operator Overloading

Operators are themselves functions and can be overloaded. In Python the arithmetic operators are already overloaded, since floats and integers are different within the computer. The addition operator + is also overloaded for other purposes, such as to concatenate strings or lists.

print(1+2)
print(1.+2.)
print("1"+"2")

We can overload operators in our classes so that we can, say, add two instances of our class. Of course “adding” the instances should make sense. Python defines a number of special methods which are distinguised by having a double underscore before and after the name; for this reason they are sometimes called “dunders” or they may be called “magic methods.” We have already encountered the __init__ dunder but there are many others.

Example We would like to create a Point class to define points in a three-dimensional Euclidean space. Points are added by adding corresponding components; i.e. $$ p1=(1,2,3),\ p2=(7,8,9),\ p1+p2=(8,10,12) $$

To implement addition we will use the __add__ dunder. This dunder will take self as its first argument, another instance of Point as its second, and it must return another instance of the class Point, so we invoke the constructor in the dunder.

class Point:
    def __init__(self,x,y,z):
        self.x=x
        self.y=y
        self.z=z

    def __add__(self,p2):
        return Point(self.x+p2.x,self.y+p2.y,self.z+p2.z)

p1=Point(1,2,3)
p2=Point(7,8,9)

p3=p1+p2
print(p3.x,p3.y,p3.z)

We could similarly define subtraction with __sub__.

Exercise

Implement subtraction for points using the rule $x_1-x_2$, $y_1-y_2$, $z_1-z_2).

Example solution

class Point:
    def __init__(self,x,y,z):
        self.x=x
        self.y=y
        self.z=z

    def __add__(self,p2):
        return Point(self.x+p2.x,self.y+p2.y,self.z+p2.z)

    def __sub__(self,p2):
        return Point(self.x-p2.x,self.y-p2.y,self.z-p2.z)

p1=Point(1,2,3)
p2=Point(7,8,9)

p3=p1+p2
print(p3.x,p3.y,p3.z)

p4=p1-p2
print(p4.x,p4.y,p4.z)

We’d like to be able to print a Point object in the standard mathematical format $(x,y,z)$. To allow print to handle our class we overload the __str__ dunder. The __str__ dunder is used by str, print, and format.

class Point:
    def __init__(self,x,y,z):
        self.x=x
        self.y=y
        self.z=z

    def __add__(self,p2):
        return Point(self.x+p2.x,self.y+p2.y,self.z+p2.z)

    def __str__(self):
        return "("+str(self.x)+","+str(self.y)+","+str(self.z)+")"

p1=Point(1,2,3)
p2=Point(7,8,9)

p3=p1+p2
print(p3)

There are many other dunders we can use in our classes. Multiplication and division don’t make sense for points, but they can be defined with __mul__ and __truediv__ respectively.

If we define the comparison dunders then we can invoke sort on our class instances.

A list of the most widely used magic methods is here.

Resources

A longer discussion of OOP in Python is available here.

Previous
Next