Python is a popular and versatile programming language that has many features that make it easy to use and powerful. However, there are also some hidden features that are not well-known or documented but can be very useful in certain situations. In this article, we will explore some of these hidden features and how to use them.
Underscore Variables
One of the hidden features of Python is the use of underscore variables. These are special variables that have a meaning depending on the context. Some of the common underscore variables are:
_
: This variable stores the result of the last expression evaluated in the interactive interpreter. For example, if you type2 + 3
and press enter, you can access the result (5) by typing_
and pressing enter again. This can be useful for quick calculations or testing.__
: This variable stores the result of the second to last expression evaluated in the interactive interpreter. For example, if you type2 + 3
,4 + 5
, and6 + 7
in sequence, you can access the result of4 + 5
(9) by typing__
and pressing enter.___
: This variable stores the result of the third to last expression evaluated in the interactive interpreter. For example, if you type2 + 3
,4 + 5
, and6 + 7
in sequence, you can access the result of2 + 3
(5) by typing___
and pressing enter._i
,_ii
,_iii
: These variables store the last, second to last, and third to last input lines entered in the interactive interpreter. For example, if you type2 + 3
,4 + 5
, and6 + 7
in sequence, you can access the input line4 + 5
by typing_ii
and pressing enter._iN
: This variable stores the Nth input line entered in the interactive interpreter. For example, if you type2 + 3
,4 + 5
, and6 + 7
in sequence, you can access the input line2 + 3
by typing_i1
and pressing enter.
Here is an example of using underscore variables in the interactive interpreter:
>>> 2 + 3
5
>>> _
5
>>> _ * 10
50
>>> __
5
>>> ___
0
>>> _i
'_ * 10'
>>> _ii
'_'
>>> _iii
'2 + 3'
>>> _i1
'2 + 3'
Double Underscore Methods
Another hidden feature of Python is the use of double underscore methods. These are special methods that are invoked automatically when certain operations are performed on an object. For example, when you use the +
operator to add two objects, Python internally calls the __add__
method of the first object with the second object as an argument. Similarly, when you use the len()
function to get the length of an object, Python internally calls the __len__
method of the object.
Double underscore methods are also known as magic methods or dunder methods. They allow you to customize the behavior of your objects and implement various protocols, such as arithmetic operations, comparison operations, iteration, representation, etc. Some of the common double underscore methods are:
__init__
: This method is called when an object is created from a class. It is used to initialize the attributes of the object.__str__
: This method is called when an object is converted to a string using thestr()
function or theprint()
function. It is used to return a human-readable representation of the object.__repr__
: This method is called when an object is converted to a string using therepr()
function or the interactive interpreter. It is used to return a machine-readable representation of the object.__add__
: This method is called when an object is added to another object using the+
operator. It is used to define how two objects are added together.__sub__
: This method is called when an object is subtracted from another object using the-
operator. It is used to define how two objects are subtracted from each other.- …and many more.
Here is an example of defining a custom class that implements some double underscore methods:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Point(self.x - other.x, self.y - other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # (1, 2)
print(repr(p1)) # Point(1, 2)
print(p1 + p2) # (4, 6)
print(p1 - p2) # (-2, -2)
Generators
Another hidden feature of Python is generators. Generators are functions that produce a sequence of values lazily. Generators allow you to create iterators without storing all the values in memory at once. For example, suppose you want to create an iterator that generates the Fibonacci sequence indefinitely. You can use a generator function to do this:
def fibonacci():
a = 0
b = 1
while True:
yield a
a, b = b, a + b
The keyword yield
is what makes a function a generator. It pauses the function and returns the current value to the caller. The next time the function is resumed, it continues from where it left off.
To use a generator function, you need to create a generator object by calling the function. Then, you can iterate over the generator object using a for
loop or the next()
function. For example:
fib = fibonacci() # create a generator object
for i in range(10): # iterate over the first 10 values
print(next(fib)) # print the next value
# output:
# 0
# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34
Generators are useful when you need to create large or infinite sequences without consuming too much memory or time.
Decorators
Another hidden feature of Python is decorators. Decorators are functions that take another function as an argument and return a modified version of that function. Decorators allow you to add extra functionality to a function without changing its original code. For example, you can use decorators to add logging, caching, timing, debugging, etc., to a function.
To use a decorator, you need to define a decorator function that takes a function as an argument and returns a modified function. Then, you need to apply the decorator to the function you want to decorate using the @
syntax. For example, suppose you have a function that calculates the factorial of a number:
def factorial(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial(n - 1)
Now, suppose you want to add a logging feature to this function, so that it prints the input and output values every time it is called. You can define a decorator function that does this:
def log(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with arguments {args} and {kwargs}")
result = func(*args, **kwargs)
print(f"Returning {result} from {func.__name__}")
return result
return wrapper
Then, you can apply the decorator to the factorial function using the @
syntax:
@log
def factorial(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial(n - 1)
Now, if you call the factorial function, you will see the logging messages:
>>> factorial(5)
Calling factorial with arguments (5,) and {}
Calling factorial with arguments (4,) and {}
Calling factorial with arguments (3,) and {}
Calling factorial with arguments (2,) and {}
Calling factorial with arguments (1,) and {}
Returning 1 from factorial
Returning 2 from factorial
Returning 6 from factorial
Returning 24 from factorial
Returning 120 from factorial
120
List Comprehensions
Another hidden feature of Python is list comprehensions. List comprehensions are a concise and elegant way to create lists from other iterables. List comprehensions allow you to apply a transformation or a filter to each element of an iterable and collect the results in a new list. For example, suppose you have a list of numbers and you want to create a new list that contains only the even numbers from the original list. You can use a list comprehension to do this:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [n for n in numbers if n % 2 == 0]
print(even_numbers)
# [2, 4, 6, 8, 10]
The syntax of a list comprehension is:
[expression for variable in iterable if condition]
The expression is the transformation that is applied to each element of the iterable. The variable is the name of the variable that holds each element of the iterable. The iterable is any object that can be iterated over, such as a list, a tuple, a string, etc. The condition is an optional filter that determines which elements are included in the new list.
List comprehensions can also be nested, meaning that you can use another list comprehension inside a list comprehension. For example, suppose you have a list of lists that represents a matrix and you want to create a new list that contains the transpose of the matrix. You can use a nested list comprehension to do this:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transpose = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
print(transpose)
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
The syntax of a nested list comprehension is:
[[expression for variable in iterable if condition] for variable in iterable if condition]
The outer list comprehension creates a new list for each column of the matrix. The inner list comprehension creates a new list for each row of the matrix. The expression is row[i]
, which accesses the ith element of each row. The variable row
holds each row of the matrix. The iterable is matrix
, which is the original list of lists. The condition is omitted in this case.
Context Managers
Another hidden feature of Python is context managers. Context managers are objects that manage the context of a block of code. Context managers allow you to perform some actions before and after the block of code executes. For example, context managers can be used to open and close files automatically, acquire and release locks or resources, enter and exit transactions or sessions, etc.
To use a context manager, you need to use the with
statement followed by an expression that evaluates to a context manager object. Then, you need to write the block of code that you want to execute in the context. For example, suppose you want to open a file, read its contents, and close it. You can use the built-in open()
function as a context manager to do this:
with open("file.txt", "r") as f: # open the file as a context manager
data = f.read() # read the file contents
print(data) # print the data
# the file is automatically closed when the block ends
The syntax of a context manager is:
with expression as variable:
block of code
The expression is any object that implements the context manager protocol. The variable is an optional name that holds the value returned by the expression. The block of code is the code that you want to execute in the context.
The context manager protocol consists of two methods: __enter__
and __exit__
. The __enter__
method is called when the with
statement is executed and returns a value that is assigned to the variable. The __exit__
method is called when the block of code ends or an exception occurs and performs any cleanup actions.
You can also create your own custom context managers by defining a class or a function that implements the context manager protocol. For example, suppose you want to create a context manager that measures the execution time of a block of code. You can define a class that does this:
import time
class Timer:
def __enter__(self):
self.start = time.time() # record the start time
return self # return the timer object
def __exit__(self, exc_type, exc_value, exc_traceback):
self.end = time.time() # record the end time
self.elapsed = self.end - self.start # calculate the elapsed time
print(f"Elapsed time: {self.elapsed} seconds") # print the elapsed time
def get_elapsed(self):
return self.elapsed # return the elapsed time
with Timer() as t: # use the timer as a context manager
for i in range(1000000): # do some computation
i ** 2
# output:
# Elapsed time: 0.2389998435974121 seconds
hoping that you found this article helpful for your coding endeavor.
You can learn this and much more in our Python data science course, which is your window to explore python as well as data science stuff like pandas, visualization and more, only at digipodium