5 Python Practices To Take Your Skills to the Next Level
Some neat Python tricks
Python is one of the most powerful beginner’s programming languages. It has a large development community and loads of frameworks for all sorts of applications.
If you are a self-taught programmer like me, who makes many projects just for fun, you may be accustomed to using Python as a functional programming language — just hundreds of plain old functions in a file for everything. But if you work on a similar or big project, you need to retype the thing again or copy it, if you still remember where you wrote it. Plus, there is a lot of advanced Python stuff that can make stuff much easier to code and read.
Therefore, you need to up your game and learn more about Python. I have used real-world examples from my Django project because a lot of tutorials talk about fruits, animals, cars, etc., to explain classes that are easier to understand but confusing in actual development.
Classes and Inheritance
This is the most obvious one, and you probably already know Object Oriented Programming (OOP) if you are coming from another programming language, but let’s get started with the obvious one.
Classes work as a template that you can reuse in multiple projects. Instantiation (creating objects from classes) allows you to replicate the functionality of code written inside the classes as many times as you like.
Why use classes?
It is much easier to handle class variables across functions that operate on a related thing. We can use inheritance to use/add functions to existing classes; plus we have dunder/magic functions as well, which we will discuss later. Let’s get a taste of classes with the following example:
#This code is for django's models
from django.db import models
class Ambulance(models.Model): #Inheriting models.Model. This will allow us to use all of its functions.
ambulance_state = models.BooleanField()
ambulance_lat = models.DecimalField(max_digits=10, decimal_places=6)
ambulance_long = models.DecimalField(max_digits=10, decimal_places=6)
ambulance_pickup_time = models.DateTimeField("Pickup Time")
# As you can see inheriting another class allows us to reuse its fucntions and gain its properties.
abulance = Ambulance() #Instantiation, to create an object of the class and use its properties.
# In django we can use it to create row in our django table
In the above example, you can see that inheriting allows us to create models for our Django project with minimal code — and with instantiation, we can create as many objects with the same properties as we like.
Args and Kwargs
args
stand for arguments and kwargs
stands for keyword arguments. They allow us to pass multiple arguments to a function. Let’s look at an example:
# '*' is an uncpaking operator which returns an unmutable iterable tuple
# '**' unpacks a dictionary
def concatenate(*args, **kwargs): #The function can take as many arguments as you want and concatenate them
int_values = ""
string = ""
for arg in args:
int_values += str(arg)
for val in kwargs.values():
string += str(val)
return int_values, string
print(concatenate(1, 2, 3, 4, 5, 6, 7, 8, 9,
10, a="abcd", b=4352, c="safda", e=95))
This function concatenates all the arguments that are passed to it. Inside the function, args
is basically a tuple and kwargs
is a dictionary, ‘*’
and ‘**’
are unpack operators. We can use names other than args and kwargs, what matters is the operator. The above code prints — 12345678910, abcd4352safda95
We can also use these operators outside the function, like this:
values = [1,2,3,4,5]
print(values) #Prints — [1,2,3,4,5]
print(*values) #Prints — 1,2,3,4,5
Decorators
Decorators allow us to add additional functionality to functions without changing the inner workings of that function. Let’s look at an example:
#Decorator Function
def unautherized_user(view_func):
def wrapper(request, *args, **kwargs): #The wrapper function allows us to control what happens before and after our view_func() is called
if request.user.is_authenticated:
return redirect('/')
else:
return view_func(request, *args, **kwargs)
return wrapper
@unautherized_user #Calling the decorator function. This is same as doing - unautherized_user(login(request))
def login(request):
#This function can authenticate users
pass
In the above example, we call the login function whenever we want to access the login page (we do not care about the inner workings of the login function). But what if the user is already logged in? We don’t want them to access the page.
To add this functionality to the login function, we could have used an if statement inside the login function, something like the following:
if request.user.is_authenticated: return homepage
But what if we want to add this functionality to our sign-up page or other pages on our website? We will repeat the code again and again. To solve this, we use a decorator.
In our decorator function:unautherized_user()
, we create another function that does the same job as our if statement declared above. Now we can just use the Python decorator syntax @
and call the decorator function @unautherized_user
over our login function to use the functionality.
Decorators allow us to do something before a function is called, and after a function is called.
Magic Methods
It is much easier to explain what magic/dunder methods are with an example, so let's just get down to business:
class Ambulance(models.Model):
ambulance_state = models.BooleanField()
ambulance_lat = models.DecimalField(max_digits=10, decimal_places=6)
ambulance_long = models.DecimalField(max_digits=10, decimal_places=6)
ambulance_free_est_time = models.DateTimeField(
"Estimated Time for Next Patient")
ambulance_pickup_time = models.DateTimeField("Pickup Time")
def __str__(self): #when we use the class object as a string, this function will be called
if self.ambulance_state:
state = "OCCUPIED"
next_available = f"IT WILL BE AVAILABLE IN {str(self.ambulance_pickup_time)}"
else:
state = "AVAILABLE"
next_available = ""
return f"AMBULANCE IS CURRENTLY {state}"
ambulance = Ambulance()
print(ambulance)
If we print an object of Ambulance class without the __str__
method, we will get something like this-<__main__.Ambulance object at 0x10a21b910>
This output is not very useful and not something we want, so we can define a __str__
to handle what happens when we use the object as a string. We can use other magic methods such as __add__
__mod__
__int__
and others to handle what happens when we use +
,%
, andint()
respectively.
With the __str__
magic method, if we print our class object, we will get — AMBULANCE IS CURRENTLY OCCUPIED
This can be very useful because now we can define how our object behaves when we use such operators and perform various operations on it.
Generators
Generators are a much more efficient way of implementing something like a list comprehension. When we use a list comprehension, the output is stored in the memory with all the values. On the other hand, generators do not take up much space and the output is generated when required. Here’s an example:
list_a = [x for x in range(10)] #stores everything in memory before returning
list_b = (x for x in range(10)) #generators are created using ()
print(list_a) #prints - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list_b) #prints - <generator object <genexpr> at 0x10f6d0c50>
#To use generator, we need to do iterate over it
# This is because generator can only output one at a time
for x in list_b:
print(x)
We use a generator when we have to deal with a large amount of data or do not want it to take up space.
There also exist generator functions that make it much easier to work with iterators in Python. The difference between a generator and a normal function is that it has a yield
statement and can also have a return
statement. yield
works as same as return
, but it does not terminate the function and maintains its state. Here’s an example:
# generator function
def even_nums():
x = 0
while True:
x += 2
yield x
# if we simply do print(next(even_nums())) it will just print 2
# This is because everytime a new function will be created
# So we first need to assign it to a variable
num = even_nums()
for i in range(10):
print(next(num))
# This way we can print as many even numbers as we want, without using any memory
The above function will print the first ten even numbers. We can use it to print as many even numbers as we want, without consuming any memory.
I know these concepts can be tough to grasp, especially if you are getting acquainted with them for the first time. Below are links to more detailed explanations of these concepts. If you still have some doubts, please leave a comment.
Resources
Corey Schafer’s playlist on OOP in Python is below:
Sentdex’s playlist on advanced Python is below:
Thanks for reading!