Introduction
Python is a powerful programming language that's easy to learn and fun to play with. But beyond the basics, there are plenty of hidden features and tricks that can help you write more efficient and more effective Python code.
In this article, we'll uncover some of these hidden features and show you how to use them to improve your Python programming skills.
Exploring Python's Hidden Features
Python is full of hidden features, some of which are more hidden than others. These features can be incredibly useful and can help you write more efficient and readable code. However, they can also be a bit tricky to discover if you don't know where to look.
In the next few sections we'll take a look at a couple features that are both helpful to know and not known as well throughout Python programmers.
The _ (Underscore) in Python
One of Python's hidden gems is the underscore (_
). It's a versatile character that can be used in various ways in Python code.
First, it can be used as a variable in Python. It's often used as a throwaway variable, i.e., a variable that is being declared but the value is not actually used.
for _ in range(5):
print("Hello, World!")
In the above code, we're not using the variable _
anywhere, it's just there because a variable is required by the syntax of the for
loop.
Second, _
is used for ignoring the specific values. If you don’t need the specific values or the values are not used, just assign the values to underscore.
# Ignore a value when unpacking
x, _, y = (1, 2, 3) # x = 1, y = 3
Here we need both the x
and y
variables, but Python's syntax won't let us declare them without something in betwee, so we use the underscore.
Last, in the Python console, _
represents the last executed expression value.
>>> 10 + 20
30
>>> _
30
Note: The use of _
for storing the last value is specific to Python’s interactive interpreter and won’t work in scripts!
Regex Debugging via Parse Tree
Regular expressions can be complex and hard to understand. Thankfully, Python provides a hidden feature to debug them via a parse tree. The re
module in Python provides the re.DEBUG
flag which can be used to debug regular expressions.
Consider the following code:
import re
re.compile("(\d+)\.(\d+)", re.DEBUG)
This will output:
SUBPATTERN 1 0 0
MAX_REPEAT 1 MAXREPEAT
IN
CATEGORY CATEGORY_DIGIT
LITERAL 46
SUBPATTERN 2 0 0
MAX_REPEAT 1 MAXREPEAT
IN
CATEGORY CATEGORY_DIGIT
0. INFO 4 0b0 3 MAXREPEAT (to 5)
5: MARK 0
7. REPEAT_ONE 9 1 MAXREPEAT (to 17)
11. IN 4 (to 16)
13. CATEGORY UNI_DIGIT
15. FAILURE
16: SUCCESS
17: MARK 1
19. LITERAL 0x2e ('.')
21. MARK 2
23. REPEAT_ONE 9 1 MAXREPEAT (to 33)
27. IN 4 (to 32)
29. CATEGORY UNI_DIGIT
31. FAILURE
32: SUCCESS
33: MARK 3
35. SUCCESS
re.compile('(\\d+)\\.(\\d+)', re.DEBUG)
This is the parse tree of the regular expression. It shows that the regular expression has two subpatterns ((\d+)
and (\d+)
), separated by a literal dot (.
).
This can be incredibly useful when debugging complex regular expressions. It gives you a clear, visual representation of your regular expression, showing exactly what each part of the expression does.
Note: The re.DEBUG
flag does not work with the re.match()
or re.search()
functions. It only works with re.compile()
.
Ellipsis
Python's ellipsis is a unique feature that's not commonly seen in other programming languages. It's represented by three consecutive dots (...) and it's actually a built-in constant in Python. You might be wondering, what could this possibly be used for? Let's explore some of its applications.
Python's ellipsis can be used as a placeholder for code. This can be very useful when you're sketching out a program structure but haven't implemented all parts yet. For instance:
def my_func():
...
# TODO: implement this function
Here, the ellipsis indicates that my_func
is incomplete and needs to be implemented.
Python's ellipsis also plays a role in slicing multi-dimensional arrays, especially in data science libraries like NumPy. Here's how you can use it:
import numpy as np
# Create a 3D array
arr = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
# Use ellipsis to access elements
print(arr[..., 2])
This will output:
[[ 3 6]
[ 9 12]]
In this case, the ellipsis is used to access the third element of each sub-array in our 3D array.
The dir() Function
The dir()
function is another hidden gem in Python. It's a powerful built-in function that returns a list of names in the current local scope or a list of attributes of an object.
When used without an argument, dir()
returns a list of names in the current local scope. Here's an example:
x = 1
y = 2
print(dir())
This will print something like:
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'x', 'y']
Here, x
and y
are the variables we defined, and the rest are built-in names in Python.
When used with an object as an argument, dir()
returns a list of the object's attributes. For instance, if we use it with a string object:
print(dir('Hello, StackAbuse!'))
This will output:
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
These are all the methods that you can use with a string object. dir()
is a handy function when you want to explore Python objects and understand their capabilities.
Lambda Functions
Lambda functions, also known as anonymous functions, are a feature of Python that allow you to create small, one-time, unnamed functions that you can use quickly and then discard. They're perfect for when you need a function for a short period of time and don't want to bother with the full function definition syntax.
Here's how you would create a lambda function:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
multiply = lambda x, y: x * y
print(multiply(5, 4))
Output:
20
In the above example, we've created a lambda function that multiplies two numbers together. We then call the function with the numbers 5 and 4, and it returns 20.
Note: While lambda functions are powerful and convenient, they should be used sparingly. Overuse of lambda functions can lead to code that is difficult to read and debug.
Chaining Comparison Operators
Python allows you to chain comparison operators in a way that's intuitive and easy to read. This can be a real time-saver when you're writing complex comparisons.
For example, let's say you want to check if a number is between 1 and 10. Instead of writing two separate comparisons and combining them with an and
operator, you can do this:
x = 5
print(1 < x < 10)
Output:
True
In this example, 1 < x < 10
is equivalent to 1 < x and x < 10
. Python checks both comparisons and returns True
if both are true, just as if you'd used the and
operator.
Note: You can chain as many comparisons as you want in this way. For example, 1 < x < 10 < x * 10 < 100
is perfectly valid Python code.
The zip() Function
Python's zip()
function is a hidden gem that doesn't get the attention it deserves. This function, which has been part of Python since version 1.5, can make your code cleaner and more efficient by allowing you to iterate over multiple sequences in parallel.
Here's a simple example of how it works:
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
for name, age in zip(names, ages):
print(f"{name} is {age} years old.")
And the output:
Alice is 25 years old.
Bob is 30 years old.
Charlie is 35 years old.
Note: The zip()
function stops at the end of the shortest input sequence. So if your sequences aren't the same length, no exception is raised - but you may lose some data from the longer sequences.
Decorators
Decorators are another powerful feature of Python that can greatly simplify your code. Essentially, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.
Let's look at an example. Suppose you have a function that performs an operation, but you want to log when the operation starts and ends. You could add logging statements directly to the function, but that would clutter your code. Instead, you can create a decorator to handle the logging:
def log_decorator(func):
def wrapper():
print("Starting operation...")
func()
print("Operation finished.")
return wrapper
@log_decorator
def perform_operation():
print("Performing operation...")
perform_operation()
When you run this code, you'll see the following output:
Starting operation...
Performing operation...
Operation finished.
The @log_decorator
line is a decorator. It tells Python to pass the function perform_operation()
to the log_decorator()
function. The log_decorator()
function then wraps the perform_operation()
function with additional code to log the start and end of the operation.
Note: Decorators can also take arguments, which allows them to be even more flexible. Just remember that if your decorator takes arguments, you need to write it as a function that returns a decorator, rather than a simple decorator function.
Context Managers and the "with" Statement
In Python, context managers are a hidden gem that can be super useful in managing resources. They allow you to allocate and release resources precisely when you want to. The most widely used example of context managers is the with
statement.
Let's take a look at an example:
with open('hello.txt', 'w') as f:
f.write('Hello, World!')
In this example, the with
statement is used to open a file and assign it to the variable f
. The file is kept open for the duration of the with
block, and automatically closed at the end, even if exceptions occur within the block. This ensures that the clean-up is done for us.
Note: Using the with
statement is like saying, "with this thing, do this stuff, and no matter how it ends, close it out properly."
Generators and the Yield Statement
Generators are a type of iterable, like lists or tuples. They do not allow indexing but they can still be iterated through with for loops. They are created using functions and the yield
statement.
The yield
statement is used to define a generator, replacing the return
of a function to provide a result to its caller without destroying local variables.
Here's a simple generator that generates even numbers:
def even_numbers(n):
for i in range(n):
if i % 2 == 0:
yield i
for number in even_numbers(10):
print(number)
Output:
0
2
4
6
8
Unlike normal functions, the local variables are not destroyed when the function yields. Furthermore, the generator object can be iterated only once.
Note: Generators are a great way to produce data which is huge or infinite. It represents a stream of data; this feature is used in Python 3's range() function.
These hidden features of Python, context managers and generators, can make your code more efficient and readable. They are worth understanding and using in your day-to-day Python coding.
Metaclasses
In Python, everything is an object - including classes themselves. This fact leads us to the concept of metaclasses. A metaclass is the class of a class; a class is an instance of a metaclass. It's a higher-level abstraction that controls the creation and management of classes in Python.
To define a metaclass, we typically inherit from the built-in type
class. Let's take a look at a simple example:
class Meta(type):
def __new__(cls, name, bases, attrs):
print(f"Creating a new class named: {name}")
return super().__new__(cls, name, bases, attrs)
class MyClass(metaclass=Meta):
pass
Running this code, you'll see the following output:
$ python3 metaclass_example.py
Creating a new class named: MyClass
In the above example, Meta
is our metaclass that inherits from type
. It overrides the __new__
method, which is responsible for creating and returning a new object. When we define MyClass
with Meta
as its metaclass, the __new__
method of Meta
is called, and we see our custom message printed.
Note: Metaclasses can be powerful, but they can also make code more complex and harder to understand. Use them sparingly and only when necessary.
Conclusion
Python is a versatile language with a plethora of hidden features that can make your coding experience more efficient and enjoyable. From the often overlooked underscore, to the powerful and complex concept of metaclasses, there's always something new to discover in Python. The key to mastering these features is understanding when and how to use them appropriately in your code.