Shubh Patni
Shubh Patni

Shubh Patni

5 Python Practices To Take Your Skills to the Next Level

Some neat Python tricks

Shubh Patni's photo
Shubh Patni
·May 13, 2021·

7 min read

5 Python Practices To Take Your Skills to the Next Level

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.

Here’s a detailed video.

Resources

Corey Schafer’s playlist on OOP in Python is below:

Sentdex’s playlist on advanced Python is below:

Thanks for reading!

 
Share this