I recently read “Effective Python: 50 Specific Ways To Write Better Python” by Brett Slatkin. Here are my personal takeaways abbreviated in a doc. I highly recommend reading Slatkin, it is a fantastic book.
Slicing deals properly with start
and end
indexes. That are beyond the boundaries of a list.
foo = ['a','b','c','d']
>>> foo[-20]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> foo[-20:]
['a', 'b', 'c', 'd']
Forms of slicing clear to a new reader of your code.
foo = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
>>> foo[:] ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
>>> foo[:5] ['a', 'b', 'c', 'd', 'e']
>>> foo[:-1] ['a', 'b', 'c', 'd', 'e', 'f', 'g']
>>> foo[4:] ['e', 'f', 'g', 'h']
>>> foo[-3:] ['f', 'g', 'h']
>>> foo[2:5] ['c', 'd', 'e']
>>> foo[2:-1] ['c', 'd', 'e', 'f', 'g']
>>> foo[-3:-1] ['f', 'g']
a = [1,2,3,4,5,6,7,8,9.10]
squares = [x**2 for x in a]
print(squares)
>>>
[1,4,9.16,25,36,49,64,81,100]
Instead of squares = map(lambda x: x ** 2, a)
. Unlike map
List Comprehensions let you easily filter items from the input list, removing corresponding outputs from the result.
even_squares = [x**2 for x in a if x % 2 == 0]
print(even_squares)
>>>
[4,16,36,64,100]
The filter
built-in function can be used along with map
to acheive the same outcome, but it is much harder to read.
my_lists = [
[[1,2,3], [4,5,6]]
]
flat = [x for sublist1 in my_lists
for sublist2 in sublist 1
for x ub sublist2]
# OR
flat = []
for sublist1 in my_lists:
for sublist2 in sublist1:
flat.extend(sublist2)
Reading a file and returning the number of characters on each line: Using List Comprehension
would require holding the length of every line of the file in memory which will cause many problems at large scale. Generator Expressions
don’t materialize the whole output sequence when they’re run. Instead, generator expressions evaluate to an iterator that yields one item at a time from the expression.
# List Comprehension
value = [len(x) for x in open('my_file.txt')]
print(value)
>>>
[100,57,15,1,12,75,5,86,89,11]
# Generator Expression
it = (len(x) for x in open('my_file.txt'))
print(it)
>>>
<generator object <genexpr> at 0x101b81480>
print(next(it))
print(next(it))
>>>
100
57
Another powerful outcome of generator expressions is that they can be composed together. E.g. Taking the iterator returned by the generator expression above and using it as input for another generator expression.
roots = ((x, x**0.5) for x in it)
print(next(roots))
>>>
(15, 3.872983346207417)
Often, you’ll want to iterate over a list and also know the index of the current item in the list. For example, say you want to print the ranking of your favourite ice cream flavors. One way to do it using range.
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print('%d: %s' % (i + 1, flavor))
# OR
for i, flavor in enumerate(flavor_list):
print('%d: %s' % (i + 1, flavor))
for i, flavor in enumerate(flavor_list, 1):
print('%d: %s' % (i, flavor))
enumerate
provides concise syntax for looping over an interator and getting the index of each item from the iterator as you go. You can supply a second parameter to enumerate
to specify the number from which to being counting from (zero is the default).
>>> for i in enumerate(flavor_list):
... print(i)
...
(0, 'vanilla')
(1, 'chocolate')
(2, 'pecan')
(3, 'strawberry')
>>> for i in enumerate(flavor_list, 1):
... print(i)
...
(1, 'vanilla')
(2, 'chocolate')
(3, 'pecan')
(4, 'strawberry')
To iterate over multiple lists in parallel, zip
is preferred. In Python 3, zip
is a lazy generator that produces tuples. In Python 2, zip
returns the full result as a list of tuples. zip
truncates the output silently if you supply it with iterators of different lengths.
>>> names = ['Celia', 'Lise', 'Marie']
>>> letters = [len(n) for n in names]
>>>
>>> longest_name = None
>>> max_letters = 0
>>>
>>> for i, name in enumerate(names):
... count = letters[i]
... longest_name = name
... max_letters = count
...
>>> for name, count in zip(names, letters):
... print(name)
... print(count)
...
Celia
5
Lise
4
Marie
5
try
except
finally
BlocksThere are four distinct times that you may want to take action during exception handling in Python. These are captured in the functionality of try
except
else
and finally
blocks.
Use try/finally when you want exceptions to propagate up but you also want to run cleanup code
handle = open('random_data.txt') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
finally:
handle.close() # Always runs after try
Use try/except/else to make it clear which exceptions will be handled by your code and which will propagate up
When the try
block doesn’t raise an exception the else
block will run. The else
block helps you minimize the amount of code in the try
block and improves readability.
def load_json_key(data, key):
try:
result_dict = json.loads(data) # May raise ValueError
except ValueError as e:
raise KeyError from e
else:
return result_dict[key] # May raise KeyError
The else
block helps you minimize the amount of code in try
blocks and visually distinguish the success case from the try
except
blocks. An else
block can be used to perform additional actions after a successful try
block but before common cleanup in a finally
block.