Effective Python #2 | Lists and Dictionaries


Introduction

  • In Python, the most common way to automate repetitive tasks is by using a sequence of values stored in a list type.

  • A natural complement to lists is the dict type, which stores lookup keys mapped to corresponding values (in what is often called an associative array or a hash table



Item 11. Know How to Slice Sequences

  1. Avoid being verbose when slicing: Don’t supply 0 for the start index or the length of the sequence for the end index.

  2. Slicing is forgiving of start or end indexes that are out of bounds, which means it’s easy to express slices on the front or back boundaries of a sequence (like a[:20] or a[-20:])

  3. Assigning to a list slice replaces that range in the original sequence with what’s referenced even if the lengths are different.



Item 12. Avoid Striding and Slicing in a Single Expression

  1. Specifying start, end, and stride in a slice can be extremely confusing.

  2. Prefer using positive stride values in slices without start or end indexes. Avoid negative stride values if possible.

  3.  Avoid using start, end, and stride together in a single slice. If you need all three parameters, consider doing two assignments (one to stride and another to slice) or using islice from the itertools built-in module.



Item 13: Prefer Catch-All Unpacking Over Slicing

  1. Unpacking assignments may use a starred expression to catch all values that weren’t assigned to the other parts of the unpacking pattern into a list.
  2. Starred expressions may appear in any position, and they will always become a list containing the zero or more values they receive.
  3. When dividing a list into non-overlapping pieces, catch-all unpacking is much less error prone than slicing and indexing.
# AS-IS
oldest = car_ages_descending[0]
second_oldest = car_ages_descending[1]
others = car_ages_descending[2:]
# TO-BE
oldest, second_oldest, *others = car_ages_descending


Item14. Sort by Complex Criteria Using the key Parameter

  1. The sort method of the list type can be used to rearrange a list’s contents by the natural ordering of built-in types like strings, integers, tuples, and so on.

  2. The sort method doesn’t work for objects unless they define a natural ordering using special methods, which is uncommon.

  3. The key parameter of the sort method can be used to supply a helper function that returns the value to use for sorting in place of each item from the list.

  4. Returning a tuple from the key function allows you to combine multiple sorting criteria together. The unary minus operator can be used to reverse individual sort orders for types that allow it.

  5. For types that can’t be negated, you can combine many sorting criteria together by calling the sort method multiple times using different key functions and reverse values, in the order of lowest rank sort call to highest rank sort call.

# 1) natural ordering
numbers = [93, 86, 11, 68, 70]
numbers.sort() # >> [11, 68, 70, 86, 93]

# 3) sorting by key param
tools = [ Tool('level', 3.5),Tool('hammer', 1.25), Tool('screwdriver', 0.5), Tool('chisel', 0.25) ]
tools.sort(key=lambda x: x.name)

# 4) multiple criteria, reverse sort
power_tools = [Tool('drill', 4), Tool('circular saw', 5), Tool('jackhammer', 40),Tool('sander', 4),]
power_tools.sort(key=lambda x: (x.weight, x.name))  # >> Tool('drill',4), Tool('sander',4), Tool('circular saw', 5), Tool('jackhammer', 40)
power_tools.sort(key=lambda x: (-x.weight, x.name)) # >> Tool('jackhammer', 40), Tool('circular saw', 5), Tool('drill', 4), Tool('sander', 4)


Item15. Be Cautious When Relying on dict Insertion Ordering

  1. Since Python 3.7, you can rely on the fact that iterating a dict instance’s contents will occur in the same order in which the keys were initially added.

  2. Python makes it easy to define objects that act like dictionaries but that aren’t dict instances. For these types, you can’t assume that insertion ordering will be preserved.

  3. There are three ways to be careful about dictionary-like classes:

    • Write code that doesn’t rely on insertion ordering,

    • explicitly check for the dict type at runtime,

    • or require dict values using type annotations and static analysis.



Item16. Prefer get Over in and KeyError to Handle Missing Dictionary Keys

  • There are four common ways to detect and handle missing keys in dictionaries: using in expressions, KeyError exceptions, the get method, and the setdefault method.

  • The get method is best for dictionaries that contain basic types like counters, and it is preferable along with assignment expressions when creating dictionary values has a high cost or may raise exceptions.

  • When the setdefault method of dict seems like the best fit for your problem, you should consider using defaultdict instead. (Item 17)

# before : in
if key in counters:
    count = counters[key]
else:
    count = 0
counters[key] = count + 1

# before : KeyError
try:
    count = counters[key]
except KeyError:
    count = 0
counters[key] = count + 1

# after : get
count = counters.get(key, 0)
counters[key] = count + 1


Item17. Prefer defaultdict Over setdefault to Handle Missing Items in Internal State

  1. If you’re creating a dictionary to manage an arbitrary set of potential keys, then you should prefer using a defaultdict instance from the collections built-in module if it suits your problem.

  2. If a dictionary of arbitrary keys is passed to you, and you don’t control its creation, then you should prefer the get method to access its items. However, it’s worth considering using the setdefault method for the few situations in which it leads to shorter code.

# before : Define funtion to count number of each alphabetical letter using "in" expression
def countLetters(word):
    counter = {}
    for letter in word:
        if letter not in counter:
            counter[letter] = 0
        counter[letter] += 1
    return counter

# after1 : using dict.setderault()
def countLetters(word):
    counter = {}
    for letter in word:
        # flaw : call setdefalut for every loop iteration
        counter.setdefault(letter, 0)
        counter[letter] += 1
    return counter

# after2 : using collections.defaultdict => int() returns 0 
from collections import defaultdict
def countLetters(word):
    counter = defaultdict(int)
    for letter in word:
        counter[letter] += 1
    return counter


Item 18. Know How to Construct Key-Dependent Default Values with __missing__

  1. The setdefault method of dict is a bad fit when creating the default value has high computational cost or may raise exceptions.

  2. The function passed to defaultdict must not require any arguments, which makes it impossible to have the default value depend on the key being accessed.

  3. You can define your own dict subclass with a __missing__ method in order to construct default values that must know which key was being accessed.