Effective Python by Brett Slatkin Review and Summary

90 Specific Ways to Write Better Python

Effective Python, following in the same vein as the other “Effective” programming books, has a list of best practices to follow for becoming proficient in this particular programming language. Brett Slatkin has provided 90 very thorough examples to help boost your Python 3.x skills ranging from the most basic of things like Item 1: Know Which Version of Python You’re Using, to more esoteric things like Item 51: Prefer Class Decorators Over Metaclasses for Composable Class Extensions.

Overall I found the book to be pretty solid and would recommend it to anyone that’s either incredibly late to the game in hopping to Python 3.x now that Python 2.7 has been a dead language for a year and a half, or to someone that’s taken an introductory Python course and has played with the language for a little while and wants to get better.

I have worked through all of the examples in the book and created iPython notebooks from them which can be found in my GitHub repository. I would encourage you to check out the notebooks to see if purchasing the book would be a good option for you (I think it would be).

Select Code Snippets

Item 4: Prefer Interpolated F-Strings Over C-Style Format Strings and str.format

pantry = [
    ('avocados', 1.25),
    ('bananas', 2.5),
    ('cherries', 15),
]

# comparing C-style, format and f-string formatting
for i, (item, count) in enumerate(pantry):
    old_style = '#%d: %-10s = %d' % (i+1, item.title(), round(count))
    
    new_style = '#{}: {:<10s} = {}'.format(i+1, item.title(), round(count))
    
    f_string = f'#{i+1}: {item.title():<10s} = {round(count)}'
    
    print(old_style)
    print(new_style)
    print(f_string)
#1: Avocados   = 1
#1: Avocados   = 1
#1: Avocados   = 1
#2: Bananas    = 2
#2: Bananas    = 2
#2: Bananas    = 2
#3: Cherries   = 15
#3: Cherries   = 15
#3: Cherries   = 15

Item 17: Prefer defaultdict over setdefault to Handle Missing Items in Internal State

# Naive way, using setdefault
class Visits:
    def __init__(self):
        self.data = {}
        
    def add(self, country, city):
        city_set = self.data.setdefault(country, set())
        city_set.add(city)
        
visits = Visits()
visits.add('England', 'Bath')
visits.add('England', 'London')
print(visits.data)

# Better way, using defaultdict
from collections import defaultdict

class Visits:
    def __init__(self):
        self.data = defaultdict(set)
        
    def add(self, country, city):
        self.data[country].add(city)
        
visits = Visits()
visits.add('England', 'Bath')
visits.add('England', 'London')
print(visits.data)
{'England': {'Bath', 'London'}}
defaultdict(<class 'set'>, {'England': {'Bath', 'London'}})

Item 25: Enforce Clarity with Keyword-Only and Positional-Only Arguments

'''
We can require callers to be clear about their intentions by 
using keyword-only arguments, which can be supplied by keyword only, 
never by position. To do this, we use the * symbol in the 
argument list to indicate the end of positional arguemtns and 
the beginning of keyword-only arguments:
'''
def safe_division_c(number, divisor, *, 
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
            
result = safe_division_c(1.0, 0, ignore_zero_division=True)
print(result)

'''
trying to call the function requiring keyword-only arguments with 
positional arguments will fail: 
'''
#result = safe_division_c(1.0, 10**500, True, False)

'''

A problem still remains, though: Callers may specify the first 
two required arguments (number and divisor) with a mix of 
positions and keywords. If I later decide to change the 
names of these first two arguments it will break all the 
existing callers. This is especially problematic because I 
never intended for number and divisor to be part of an explicit 
interface for this function; they were just confnenient parameter 
names that I chose for the implementation, and I didn't expect 
anyone to rely on them explicitly.

Python 3.8 introduces a solution to this problem, called 
positional-only arguments. These arguments can be supplied 
only by position and never by keyword. The symbol/ in the 
argument list indicates where positional-only arguments end:
'''
def safe_divisor_d(numerator, denominator, /, *, 
                   ignore_overflow=False,
                   ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise
            
result = safe_division_d(1.0, 0, ignore_zero_division=True)
print(result)
result = safe_division_d(2, 5)
print(result)

'''
Now an exception is raised if keywords are used for the 
positional-only arguments
'''
#safe_division_d(numerator=2, denominator=5)

Item 27: Use Comprehensions Instead of map and filter

# naive way (for loop and list.append)
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = []
for x in a:
    squares.append(x**2)
print(squares)

# slightly better way (using map built-in function)
alt_squares = map(lambda x: x**2, a)
print(list(alt_squares))

# best way (list comprehensions)
alt_squares2 = [x**2 for x in a]
print(alt_squares2)

# Unlike map, list comprehensions let you easily filter items from the input list:
even_squares = [x**2 for x in a if x % 2 == 0]
print(even_squares)

# The filter built in function can be used along with map to achieve the same result, but is much harder to read:
alt = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))
print(list(alt))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[4, 16, 36, 64, 100]
[4, 16, 36, 64, 100]

Item 37: Compose Classes Instead of Nesting Many Levels of Built-in Types

from collections import namedtuple, defaultdict

# named tuple to represent a simple grade
Grade = namedtuple('Grade', ('score', 'weight'))

class Subject:
    """ Class to represent a single subject that contains a set of grades."""
    def __init__(self):
        self._grades = []
        
    def report_grade(self, score, weight):
        self._grades.append(Grade(score, weight))
        
    def average_grade(self):
        total, total_weight = 0, 0
        for grade in self._grades:
            total += (grade.score * grade.weight)
            total_weight += grade.weight
        return total / total_weight
    
class Student:
    """ Class to represent a set of subjects that are studied by a single student."""
    def __init__(self):
        self._subjects = defaultdict(Subject)
        
    def get_subject(self, name):
        return self._subjects[name]
    
    def average_grade(self):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
            
        return total / count
    
class GradeBook:
    """ 
    Class to represent a container for all of the students, 
    keyed dynamically by their names.
    """
    def __init__(self):
        self._students = defaultdict(Student)
        
    def get_student(self, name):
        return self._students[name]
    
    
book = GradeBook()
albert = book.get_student('Albert Einstein')

math = albert.get_subject('Math')
math.report_grade(75, 0.05)
math.report_grade(65, 0.15)
math.report_grade(70, 0.80)

gym = albert.get_subject('Gym')
gym.report_grade(100, 0.40)
gym.report_grade(85, 0.60)

print(albert.average_grade())
80.25

Leave a Reply

Your email address will not be published. Required fields are marked *