Reading Time: 6 minutesObject Oriented Programming in Python
Object Oriented Programming in Python
Table of Contents
Hi. In this chapter, we'll look at the building blocks of Object Oriented Programming. Towards the end of this chapter, you will have developed understanding of Object Oriented Programming in Python and how classes work in Python.
Classes§
- A class is an instance factory. It defines the template or blueprint of its instances.
- It defines the functionality and properties each of its object will have.
- A class is not supposed to have any substantial functionality unless it is instantiated.
- A class in Python is defined using the keyword class. By convention, class names begin with an uppercase letter, to distinguish them from variables and functions.
< class '__main__.Man' > |
The __main__ refers to the current module being executed. When a module is executed, its __name__ attribute is set to the string '__main__', implying that it is being run directly and not being imported. Had the class been defined in an imported module, say people, the >>> Man prompt would have returned <class 'people.Man'>.
Instances§
- An instance is the manufactured product of the instance factory i.e. class.
- It is created by calling the class.
- It holds data in its attributes, known as instance attributes, and has functionality associated to this data, using the methods defined in the class definition. These methods are known as instance methods.
- It knows which class it belongs to (print(instanceName) or instanceName.__class__.__name__).
- It can access the attributes defined in the class. The attributes defined in a class can either be class attributes or instance attributes, instances have access to both kinds of attributes.
>>> ethan = Man() |
>>> print (ethan) |
<__main__.Man object at 0x02FA9150 > |
>>> print (ethan.__class__) |
< class '__main__.Man' > |
>>> print (ethan.__class__.__name__) |
Instance Methods§
- Instance Methods are defined in the class.
- They are accessed via syntax instanceName.methodName().
- A method called by the instance passes the same instance as the first argument to itself (i.e. the method) automatically. This first argument is an implicit argument, that is, it does not need to be explicitly passed like other arguments, the method being called does this behind the scenes. This first argument is often called self in the class, owing to popular convention. However, you can call it whatever you like, but it's advisable that you call it self, for readability for others. Since the instance is passed automatically to the method, the instance methods are called bound methods, meaning that they are bound to the instance which is calling the method. The example below will illustrate what I'm saying.
return "Hi, I am eating." |
>>> ethan = Man() |
<__main__.Man object at 0x03110050 > |
<__main__.Man object at 0x03110050 > |
return "Hi, I am eating." |
>>> ethan = Man() |
Traceback (most recent call last): |
TypeError: eat() takes 0 positional arguments but 1 was given |
As you can see, the error says that 1 positional argument was given. This positional argument, that is automatically being given is passed by the method itself when it is called by the instance, and it refers to the instance itself which is calling the method. I am emphasizing this again and again, because it is extremely important to understand what self means.
Class Methods§
- There may be methods inside a class without the self argument. These are class-exclusive methods.
- They will take as many arguments when they are being called, as they are defined with, leaving no room for the implicit argument self.
- These do not form the functionality of the instances as they cannot be called by them, due to absence of the self argument while defining the methods. These can only be called by the class using notation className.methodOne().
return "Hi, I am eating." |
>>> ethan = Man() |
Traceback (most recent call last): |
TypeError: eat() takes 0 positional arguments but 1 was given |
Instance Attributes§
- Each instance has its own data, stored in the form of their attributes. These are different from the class attributes, which we study in the next section.
- The instance attributes are defined in the class methods with self as prefix, signifying that the attribute being tweaked belongs to the instance in context.
- Instance attributes are accessed as instanceName.attributeName.
- Instance attributes can be defined in any method defined in the class, but they are usually defined in the __init__() method (which we will discuss later in the chapter), which is called automatically when the instance is being created.
- An instance can have methods in itself to set and get values.
- The instance attributes are often termed as the state of the instance as these attributes reflect the current state of the object.
- Entire list of attributes of instance can be viewed by calling its dunder dict attribute i.e. __dict__ or using the builtin vars() method >>> vars(instanceName). Double-underscore attributes or dunder attributes are default attributes that Python gives every object of a class. You can read about the dunder attributes here. The builtin vars() function, when called without an object as its argument, calls the __dict__ of the object and returns the dictionary. When called without an argument, it returns a dictionary of local variables.
def sayMyName( self , passedName): |
print ( "My name is {}." . format ( self .name)) |
>>> ethan = Man() |
>>> ethan.sayMyName( 'Ethan' ) |
>>> ethan.__dict__ |
Class attributes §
- There maybe attributes on the class level as well. While defining, these attributes are not prefixed by self, as these are the property of the class and not of a particular instance.
- The class attributes can be accessed by the class itself (className.attributeName) as well as by the instances of the class(inst.attributeName). So, the instances have access to both the instance attributes as well as class attributes.
- When an attribute is being accessed by the instance, Python looks for it in the instance attributes, and then in the class attributes. The example below will illustrate this point.
- While setting and getting class attributes inside the class itself, these are prefixed with the class name (className.attributeName). This is done for two reasons. One, normal assignment (i.e. without the className as prefix) inside methods leads Python to interpret that the variable in context is a local variable inside that method. Second, to distinguish the class attribute from global variables. The variables set in the main body of the script are called global variables. The builtin globals() function returns a dictionary of these variables. In order for classes to work with global variables, the class attributes need to be distinguished from them. To achieve this, we explicitly tell Python that we are accessing attribute attributeName of class className.
>>> ethan = Man() |
>>> ethan.century |
def setInstanceCentury( self , num): |
>>> ethan = Man() |
>>> ethan.setInstanceCentury( 23 ) |
>>> ethan.century |
>>> del ethan.century |
>>> ethan.century |
def manipulateClassAtribute(): |
>>> Man.manipulateClassAtribute() |
def manipulateClassAtribute(): |
>>> Man.manipulateClassAtribute() |
>>> century = 20 |
{ 'century' : 20 , <<other global variables>>} |
def manipulateClassAtribute(): |
print ( "century before assignment in manipulateClassAtribute():" , century) |
print ( "century after assignment in manipulateClassAtribute():" , century) |
>>> Man.manipulateClassAtribute() |
century before assignment in manipulateClassAtribute(): 20 |
century after assignment in manipulateClassAtribute(): 23 |
{ 'century' : 23 , <<other global variables>>} |
Encapsulation§
- Encapsulation is a principle in Object Oriented Programming, which refers to secure storage of data in instances.
- It is one of 3 foundation principles of OOP.
- This data is stored in instance attributes, and can be manipulated from anywhere outside the class. To secure it, the data should only be accessed using instance methods. Direct access should not be permitted.
- Additionally, the data should be stored only if it is correct and valid, using Exception Handling constructs.
- Encapsulation is implemented in other programming languages differently, as these languages have concepts of private and public members of a class. Python has no such concepts, and hence, encapsulation in Python is a voluntary constraint.
>>> ethan = Man() |
>>> ethan.setAge( 23 ) |
>>> ethan.getAge() |
>>> ethan = Man() |
>>> ethan.setAge( 'Twenty three' ) |
>>> ethan.getAge() |
print ( "Please provide a number to the setAge method." ) |
>>> ethan = Man() |
>>> ethan.setAge( 'Twenty three' ) |
Please provide a number to the setAge method. |
>>> ethan.setAge( 23 ) |
>>> ethan.getAge() |
__init__ & __str__ magic methods§
The __init__ method is one of several class methods which are called implicitly on certain events. These methods are called magic methods, or dunder methods, because of the double underscore in front of the method name. These methods need not be defined, but if defined, Python will execute them on certain events. The __init__() method is called when an instance is created.
The init stands for initialization, as it initializes attributes of the instance. It is called the constructor of a class.
print ( "Hi, I am {}. I am {} years old. I weigh {} lbs and I am {} cm tall." . |
format ( self .name, self .age, self .weight, self .height)) |
>>> ethan = Man() |
>>> ethan.details() |
Hi, I am Ethan. I am 10 years old. I weigh 60 lbs and I am 100 cm tall. |
def __init__( self , name, age, weight, height): |
print ( "Hi, I am {}. I am {} years old. I weigh {} lbs and I am {} cm tall." . |
format ( self .name, self .age, self .weight, self .height)) |
>>> ethan = Man( 'Ethan' , 10 , 60 , 100 ) |
>>> ethan.details() |
Hi, I am Ethan. I am 10 years old. I weigh 60 lbs and I am 100 cm tall. |
The __str__ magic method is called when the object is printed, for example, while printing it. The __str__ is defined to return a string representation of the instance.
def __init__( self , name, age, weight, height): |
>>> ethan = Man( 'Ethan' , 10 , 60 , 100 ) |
1. <__main__.Man object at 0x033D0710 > |
>>> print (ethan) |
<__main__.Man object at 0x033D0710 > |
def __init__( self , name, age, weight, height): |
return "{} | {} | {} | {}" . format ( self .name, self .age, self .weight, self .height) |
>>> ethan = Man( 'Ethan' , 10 , 60 , 100 ) |
>>> print (ethan) |
Exercise: Handling class and instance data - Implement a counter§
We have reached the end of the first chapter, and I hope you've developed an understanding of classes. Classes, Instances, Instance Attributes, Class Attributes, Instance Methods, Class Methods, __init__(), __str__() and Encapsulation. All of this is essential going forward.
Let's put our knowledge to use with an exercise. A common task in Object Oriented Programming is to maintain a counter of number of objects that have been instantiated from a class. Here's the problem statement: We have a class Toy. Each toy will have two attributes: name and a color. The user needs to be given the ability to change the color of the toy. Each toy will have a method speak which will print the name of the toy. When a toy is created, we wish to print the number of toys manufactured so far.
So, this is essentially what we will need to materialize the solution for the above problem.
counter variable set to 0 |
def __init__( self , name, color): |
sets instance attributes 'name' and 'color' to provided values; increments counter and prints it. |
def setColor( self , newColor): |
sets instance attribute 'color' to the provided value. |
returns the instance attribute 'color' . |
returns a string with instance attribute 'name' . |
Now, I want you to try to write the code. Go ahead. I am sure you'll be able to do it. Worst that will happen that you will run into some errors, but it's worth it. You'll learn something new. Eventually, you'll get there. I am listing the solution here, so that you can tally your solution. The following is not necessarily the best solution out there. There may be others as well.
def __init__( self , name, color): |
print ( "Toys manufactured so far:" , Toy.count) |
def setColor( self , newColor): |
return "Hi, my name is {}." . format ( self .name) |
>>> woodie = Toy( 'Woodie' , 'Brown' ) |
Toys manufactured so far: 1 |
>>> woodie.setColor( 'Blue' ) |
>>> woodie.getColor() |
>>> woodie.speak() |
>>> buzz = Toy( 'Buzz Lightyear' , 'White & Purple' ) |
Toys manufactured so far: 2 |
>>> hamm = Toy( 'Hamm The Piggy Bank' , 'Light Pink' ) |
Toys manufactured so far: 3 |
It's really important to dwell on the following points when you are designing the solution to any problem.
- Readability of code
- Maintainability of code
- Encapsulate the data
- Code conventions, such as variable naming and two blank lines before the class definition (PEP 8)
- DRY: Don't repeat yourself
- Time taken to execute the script
- Storage resources
On the Agenda in the Next Chapter§
That's it for this one. Next up, we take on Inheritance, Abstract Classes & Polymorphism. Till then, goodbye!