Python Refresher: The Essentials
Exploring Python's Landscape: Object-Oriented Principles, Exception Handling, and Beyond
If you need a quick refresher on the basics - you can head over to "Python Refresher: The Basics" post, where I've gone through the basic stuff from syntax and variables to data structures. But if you're ready, let's explore some object-oriented features and more!
Object-Oriented Programming (OOP) in Action:
Python is an object-oriented language. Every piece of data and all functions in Python are considered objects. Let's break down what this means...
Each object is an instance of a class. You can create your classes using the class
keyword and form methods within that class.
class MyClass:
x = 5
y = "Why?"
p1 = MyClass()
print(p1.x)
print(p1.y)
There are four main principles in OOP: encapsulation, inheritance, polymorphism, and abstraction.
Encapsulation:
Encapsulation is like a magician's hat, hiding the bunny - in this case, data - inside. Python can restrict access to methods and variables. This prevention of direct data modification is encapsulation. You can't see it, but it's happening.
Python uses a naming convention for private variables with a single underscore prefix (_var) or double underscore prefix (__var).
A single underscore prefix is a convention used to indicate that a variable or method is intended for internal use within the class, module, or function. It doesn't prevent external access or modification, but it serves as a hint to the programmer that it's considered to be internal.
A double underscore prefix is a stronger indication that a variable or method should not be accessed directly. Python alters the name of any variable or method that starts with two underscores, which makes it harder (but not impossible) to access it directly.
Here is an example of encapsulation using private variables:
class MyClass:
__hiddenVariable = 0 # double underscore for a stronger private hint
def add(self, increment):
self.__hiddenVariable += increment
print(self.__hiddenVariable)
myObject = MyClass()
myObject.add(5)
# Accessing directly would lead to AttributeError: 'MyClass' object has no attribute '__hiddenVariable'
Note that direct access to __hiddenVariable
is still possible with the mangled name, but it's generally discouraged:
print(myObject._MyClass__hiddenVariable) # prints 5, but don't do this!
Inheritance:
Inheritance is like the phrase "like father, like son." A new class can adopt details from an existing class, without altering it. The new class is referred to as a derived (or child) class, and the class from which it inherits is known as the base (or parent) class.
class Parent: # define parent class
parentAttr = 100
def __init__(self):
print("Calling parent constructor")
class Child(Parent): # define child class
def __init__(self):
print("Calling child constructor")
c = Child()
print(c.parentAttr)
Polymorphism:
Polymorphism lets you define methods in the child class with the same name as those defined in their parent class. It enables us to utilize the same interface for different data types.
class Animal:
def type(self):
pass
class Dog(Animal):
def type(self):
return "Dog"
class Cat(Animal):
def type(self):
return "Cat"
animals = [Dog(), Cat()]
for animal in animals:
print(animal.type())
Abstraction:
Abstraction is like a magician's cloak – it hides the complicated parts and shows only what's essential. It is a process of hiding the implementation details and displaying only functionality to the user.
from abc import ABC, abstractmethod
class AbstractVehicle(ABC):
@abstractmethod
def speed(self):
pass
@abstractmethod
def type_of_vehicle(self):
pass
class Car(AbstractVehicle):
def speed(self):
return "100 km/h"
def type_of_vehicle(self):
return "Land vehicle"
class Plane(AbstractVehicle):
def speed(self):
return "900 km/h"
def type_of_vehicle(self):
return "Air vehicle"
vehicles = [Car(), Plane()]
for vehicle in vehicles:
print(f'Type: {vehicle.type_of_vehicle()} - Speed: {vehicle.speed()}')
Exception Handling: Turning Obstacles into Opportunities
When your code encounters an error, it throws an exception. However, Python equips you with the try
, except
, and finally
keywords to handle these unexpected events gracefully.
try:
print(10/0)
except ZeroDivisionError:
print("You can't divide by zero!")
finally:
print("This will run no matter what.")
Iterators:
Iterators return one item at a time, maintaining their position and advancing forward.
my_tuple = ("apple", "banana", "cherry")
my_iter = iter(my_tuple)
print(next(my_iter))
print(next(my_iter))
print(next(my_iter))
A Python iterator is an object and is implemented via classes. Here is an example of an iterator:
class MyNumbers:
def __iter__(self):
self.a = 1
return self
def __next__(self):
if self.a <= 7:
x = self.a
self.a += 1
return x
else:
raise StopIteration
myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:
print(x)
Generators:
Generators, like lists or tuples, are a special type of iterable. Unlike lists, they don't support indexing with arbitrary indices, but you can still iterate through them with for loops. Here is an example of a generator:
def my_gen():
n = 1
print('This is printed first')
yield n
n += 1
print('This is printed second')
yield n
n += 1
print('This is printed last')
yield n
for item in my_gen():
print(item)
Decorators:
Ever wondered how to enhance your functions without permanently altering them? Decorators have got you covered! They add a dash of magic to your functions at compile time, a process also known as metaprogramming
. A decorator takes in a function, enhances its functionality, and returns it. Here is an example of a decorator:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
def say_hi():
print("Hi!")
say_it = my_decorator(say_hi)
say_it()
File I/O:
Python comes with basic functions and methods necessary to manipulate files by default. Most file manipulations can be performed using a file
object.
# write to a file
file = open('file.txt', 'w')
file.write('Hello, world! \nThis is the 2nd line')
file.close()
# read the file
file = open('file.txt', 'r')
print(file.read())
file.close()
# read line by line
file = open('file.txt', 'r')
for line in file:
print(line, end='')
file.close()
The os
and shutil
modules provide methods for file and directory operations like creating, deleting, moving files or directories, etc.
import os
import shutil
# create a new directory
os.mkdir('new_directory')
# rename the directory
os.rename('new_directory', 'old_directory')
# remove the directory
os.rmdir('old_directory')
# copy a file
shutil.copy('file.txt', 'new_file.txt')
# move a file
shutil.move('new_file.txt', 'new_file_name.txt')
# remove the file
os.remove("new_file_name.txt")
Context Managers:
The unseen hero, known as context managers, takes care of managing resources for you. The context manager protocol involves the __enter__
and __exit__
methods.
An example of a context manager is the with
statement. It's used to ensure that resources are correctly managed and that you don't have to manually release them.
with open('file.txt', 'r') as file:
print(file.read())
List Comprehensions: Shortcut to New Lists
List comprehensions provide a concise way to create lists based on existing lists.
nums = [1, 2, 3, 4]
squared = [n ** 2 for n in nums]
print(squared) # Outputs: [1, 4, 9, 16]