Python: Hidden Features — part 1
Hi Everyone, In this article I will go throw couple of techniques or features which anyone can use in day to day coding.
In my journey through these Python features, I’ve uncovered tools that go beyond the ordinary, adding a touch of magic to our code.
Lets get started —
1. Function Objects
Everything in python is an object instantiated from some class. This also includes functions, but accepting this fact often feels counterintuitive at first.
Lets see an example —
def add_subject(name, subject, subjects=[]):
subjects.append(subject)
return {'name': name, 'subjects': subjects}
add_subject('person1', 'subject1')
add_subject('person2', 'subject2')
add_subject('person3', 'subject3')
output -
{'name': 'person1', 'subjects': ['subject1']}
{'name': 'person2', 'subjects': ['subject1', 'subject2']}
{'name': 'person3', 'subjects': ['subject1', 'subject2', 'subject3']}
Mutability in Python is possibly one of the most misunderstood and overlooked concepts.
The default parameters of a function are evaluated right at the time the function is defined. Thus, as soon as a function is defined, the function object stores the default parameters in its __defaults__
attribute.
We can verify this below —
def my_functions(a=1, b=2, c=3):
pass
print(my_functions.__defaults__)
output - (1, 2, 3)
Thus, if you specify a mutable default parameter in a function and mutate it, you unknowingly and unintentionally modify the parameter for all future calls to that function.
This is shown in the demonstration below. Instead of creating a new list at each function call, Python appends the element to the same copy.
def add_subject(name, subject, subjects=[]):
subjects.append(subject)
return {'name': name, 'subjects': subjects}
print(add_subject.__defaults__)
add_subject('person1', 'subject1')
print(add_subject.__defaults__)
add_subject('person2', 'subject2')
print(add_subject.__defaults__)
add_subject('person3', 'subject3'))
print(add_subject.__defaults__)
Output -
([],)
(['subject1'],)
(['subject1', 'subject2'],)
(['subject1', 'subject2', 'subject3'],)
How to avoid
Instead of specifying a mutable default parameter in a function’s definition, replace them with None
. If the function does not receive a corresponding value during the function call, create the mutable object inside the function.
def add_subject(name, subject, subjects=None):
if subjects is None:
subjects = []
subjects.append(subject)
return {'name': name, 'subjects': subjects}
print(add_subject.__defaults__)
print(add_subject('person1', 'subject1'))
print(add_subject('person2', 'subject1'))
print(add_subject('person3', 'subject1'))
## Output -
(None,)
{'name': 'person1', 'subjects': ['subject1']}
{'name': 'person2', 'subjects': ['subject1']}
{'name': 'person3', 'subjects': ['subject1']}
As shown above, we create a new list if the function didn’t receive any value when it was called. This lets you avoid the unexpected behavior of mutating the same object.
2. Make class object behave like a Function
If you want to make a class object callable, i.e., behave like a function, you can do so by defining the __𝐜𝐚𝐥𝐥__ method. This method allows you to define the behavior of the object when it is invoked like a function.
class Multiplier:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __call__(self, x):
return (self.a * x**2) + (self.b * x**2) + (self.c * x**2)
func = Multiplier(2, 4, 6)
print(func(2)) # 48
print(func(4)) # 192
print(callable(func)) # True
This can have many advantages. For instance, it allows us to implement objects that can be used in a flexible and intuitive way. What’s more, the familiar function-call syntax, at times, can make your code more readable.
It allows you to use a class object in contexts where a callable is expected. Using a class as a decorator, for instance.
3. String Method
Here’s something new and cool I recently learned about a string method in Python.
In Python, the startswith
and endswith
string methods are commonly used to match a substring at the start and end of the string.
places = ["India", "London", "Poland", "Netherlands"]
for place in places:
if place.startswith('Lo') or place.startswith('Po'):
print(place)
However, did you know you can also pass multiple substrings as a tuple to these methods?
places = ["India", "London", "Poland", "Netherlands"]
for place in places:
if place.startswith(('Lo', 'Po')):
print(place)
This prevents you from writing multiple conditions while preserving the same functionality.
4. Use Frozenset
Dictionaries in Python require their keys to be immutable. As a result, a set cannot be used as keys as it is mutable.
set_ex = {1, 2, 3}
dict_obj = {set_ex: "This is a Set"}
## Error - TypeError: unhashable type: 'set'
if you want to use a set, consider declaring it as a frozenset.
set_ex = frozenset({1, 2, 3})
dict_obj = {set_ex: "This is a Set"}
print(dict_obj[set_ex])
## Output - This is a Set
It is an immutable set, meaning its elements cannot be changed after it is created. Therefore, they can be safely used as a dictionary’s key.
5. Pickle Files
Pickles are widely used to dump data objects to disk. But folks often dump just a single object into a pickle file. Moreover, one creates multiple pickles to store multiple objects.
However, did you know that you can store as many objects as you want within a single pickle file? What’s more, when reloading, it is not necessary to load all the objects.
Just make sure to dump the objects within the same context manager (using 𝐰𝐢𝐭𝐡).
import pickle
a, b, c = 1, 2, 3
with open("data.pkl", "wb") as f:
pickle.dump(a, f)
pickle.dump(b, f)
pickle.dump(c, f)
import pickle
with open("data.pkl", "rb") as f:
a = pickle.load(f)
b = pickle.load(f)
print(f"{a = } {b = }")
Of course, one solution is to store the objects together as a tuple. But while reloading, the entire tuple will be loaded. This may not be desired in some cases.
Conclusion
Above are the few features which reinforce the importance of exploring beyond the basics and continuously learning in the ever-evolving Python ecosystem.
Happy coding !!