Object Oriented Programming in Python
- Classes
- Instances
- Instance Methods
- Class Methods
- Instance Attributes
- Class attributes
- Encapsulation
- __init__ & __str__ magic methods
- Exercise: Handling class and instance data - Implement a counter
- On the Agenda in the Next Chapter
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 Man: pass >>> Man <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__) Man
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.
>>> class Man: def eat(self): print(self) return "Hi, I am eating." >>> ethan = Man() >>> ethan.eat() <__main__.Man object at 0x03110050> 'Hi, I am eating.' >>> ethan <__main__.Man object at 0x03110050> # The Hex code shows that self in print(self) and ethan in >>> ethan are the same object. # When the self argument is not defined in the method. >>> class Man: def eat(): return "Hi, I am eating." >>> ethan = Man() >>> ethan.eat() Traceback (most recent call last): ethan.eat() 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().
>>> class Man: def eat(): return "Hi, I am eating." >>> ethan = Man() >>> # Cannot be called by the instance >>> ethan.eat() Traceback (most recent call last): ethan.eat() TypeError: eat() takes 0 positional arguments but 1 was given >>> # Can only be called by using the class name >>> Man.eat() 'Hi, I am eating.'
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.
>>> class Man: def sayMyName(self, passedName): self.name = passedName print("My name is {}.".format(self.name)) >>> ethan = Man() >>> ethan.sayMyName('Ethan') My name is Ethan. >>> ethan.name 'Ethan' >>> ethan.__dict__ {'name': 'Ethan'} >>> vars(ethan) {'name': 'Ethan'}
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.
# 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). >>> class Man: century = 21 >>> Man.century 21 >>> ethan = Man() >>> ethan.century 21 # When an attribute is being accessed by the instance, Python looks for it in the instance attributes, and then in the class attributes. >>> class Man: century = 21 def setInstanceCentury(self, num): self.century = num >>> ethan = Man() >>> ethan.setInstanceCentury(23) >>> ethan.century 23 >>> Man.century 21 >>> >>> del ethan.century # removing the instance attribute from memory >>> ethan.century 21 # The above value is of the class attribute, since we removed the instance attribute manually. This proves that the order in which # Python searches for an attribute is instance first, class afterwards. # 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 >>> class Man: century = 21 def manipulateClassAtribute(): century = 23 >>> Man.manipulateClassAtribute() >>> Man.century 21 # century with value 23 is a local variable inside manipulateClassAtribute(). # You can verify this by putting a print(century) statement just before century = 23. You will get the following error # UnboundLocalError: local variable 'century' referenced before assignment # If you want the class attribute's value to show up when you execute >>> Man.century, you have to prefix the attribute with the className during assignment >>> class Man: century = 21 def manipulateClassAtribute(): Man.century = 23 >>> Man.manipulateClassAtribute() >>> Man.century 23 # Second, to distinguish the class attribute from global variables. >>> century = 20 >>> globals() {'century': 20, <<other global variables>>} >>> class Man: century = 21 def manipulateClassAtribute(): global century print("century before assignment in manipulateClassAtribute():", century) century = 23 print("century after assignment in manipulateClassAtribute():", century) >>> Man.century 21 # reflects initial value of class attribute >>> Man.manipulateClassAtribute() century before assignment in manipulateClassAtribute(): 20 century after assignment in manipulateClassAtribute(): 23 >>> globals() {'century': 23, <<other global variables>>} # The global variable declared inside the class overrides the global variable outside it.
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.
# 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. >>> class Man: def setAge(self, num): self.age = num def getAge(self): return self.age >>> ethan = Man() >>> ethan.setAge(23) >>> ethan.getAge() 23 # Additionally, the data should be stored only if it is correct and valid, using Exception Handling constructs. # In the above code snippet, there is no restriction on the user over the value he is passing to setAge() method. It could be a number, a string, a list etc. For example, let's pass the age in string format. >>> class Man: def setAge(self, num): self.age = num def getAge(self): return self.age >>> ethan = Man() >>> ethan.setAge('Twenty three') >>> ethan.getAge() 'Twenty three' # Since age is supposed to be a number, we need to add constructs to ensure correctness of being stored. >>> class Man: def setAge(self, num): try: self.age = int(num) except: print("Please provide a number to the setAge method.") def getAge(self): return self.age >>> ethan = Man() >>> ethan.setAge('Twenty three') Please provide a number to the setAge method. >>> ethan.setAge(23) >>> ethan.getAge() 23
__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.
# initializing an instance with static data >>> class Man: def __init__(self): self.name = 'Ethan' self.age = 10 self.weight = 60 self.height = 100 def details(self): 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. # initializing an instance with dynamic data >>> class Man: def __init__(self, name, age, weight, height): self.name = name self.age = age self.weight = weight self.height = height def details(self): 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.
# When the __str__() is not defined >>> class Man: def __init__(self, name, age, weight, height): self.name = name self.age = age self.weight = weight self.height = height print("1.", self) >>> ethan = Man('Ethan', 10, 60, 100) 1. <__main__.Man object at 0x033D0710> # from print(self) >>> print(ethan) <__main__.Man object at 0x033D0710> # from print(instanceName) >>> >>> >>> >>> >>> # When the __str__() is defined >>> class Man: def __init__(self, name, age, weight, height): self.name = name self.age = age self.weight = weight self.height = height print("1.", self) def __str__(self): return "{} | {} | {} | {}".format(self.name, self.age, self.weight, self.height) >>> ethan = Man('Ethan', 10, 60, 100) 1. Ethan | 10 | 60 | 100 # from print(self) >>> print(ethan) Ethan | 10 | 60 | 100 # from print(instanceName)
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.
class Toy: 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. def getColor(self): returns the instance attribute 'color'. def speak(self): 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.
#! C:\Python34\python.exe class Toy: '''Toy class''' count = 0 def __init__(self, name, color): '''sets instance attributes to provided values; increments counter and prints it.''' self.name = name self.color = color Toy.count += 1 print("Toys manufactured so far:", Toy.count) def setColor(self, newColor): '''sets instance attribute color to the provided value.''' self.color = newColor def getColor(self): '''returns the instance attribute color.''' return self.color def speak(self): '''returns a string with instance attribute name.''' return "Hi, my name is {}.".format(self.name) ### UPON RUNNING THE ABOVE SCRIPT (Run Menu > Run Module/F5) ### >>> woodie = Toy('Woodie', 'Brown') Toys manufactured so far: 1 >>> woodie.setColor('Blue') >>> woodie.getColor() 'Blue' >>> woodie.speak() 'Hi, my name is Woodie.' >>> >>> 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!