Data Hiding, Inheritance, Polymorphism, and Operator Overloading
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.
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.”
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
- max_height The methods would include behaviors such as
A Tree would add members specific to an individual tree, such as
In Python, we indicate the parent with parentheses
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.
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, Animal)) >>>print(isinstance(zoo, Canine)) >>>print(isinstance(zoo, Feline))
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
Implement subtraction for points using the rule $x_1-x_2$, $y_1-y_2$, $z_1-z_2).
We’d like to be able to print a Point object in the standard mathematical format $(x,y,z)$. To allow
__str__ dunder. The
__str__ dunder is used by
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
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.
A longer discussion of OOP in Python is available here.