Brett Slatkin - Refactoring Python
Brett Slatkin - Refactoring Python
Agenda
What, When, Why, How
Strategies
Extract Variable & Function
Extract Class & Move Fields
Move Field gotchas
Follow-up
Bonus
Extract Closure
What is refactoring?
Repeatedly reorganizing and rewriting
code until it's obvious* to a new reader.
In advance
For testing
"Don't repeat yourself"
Brittleness
Complexity
Refactoring
Me
usually
Good
Great
Time spent
100%
But...
But...
Strategies
Prerequisites
Thorough tests
Quick tests
Source control
Willing to make mistakes
def what_to_eat(month):
if (month.lower().endswith('r') or
month.lower().endswith('ary')):
print('%s: oysters' % month)
elif 8 > MONTHS.index(month) > 4:
print('%s: tomatoes' % month)
else:
print('%s: asparagus' % month)
>>>
November: oysters
July: tomatoes
March: asparagus
Before
if (month.lower().endswith('r') or
month.lower().endswith('ary')):
print('%s: oysters' % month)
elif 8 > MONTHS.index(month) > 4:
print('%s: tomatoes' % month)
else:
print('%s: asparagus' % month)
or ends_in_ary:
oysters' % month)
tomatoes' % month)
asparagus' % month)
def tomatoes_good(month):
index = MONTHS.index(month)
return 8 > index > 4
Before
if (month.lower().endswith('r') or
month.lower().endswith('ary')):
print('%s: oysters' % month)
elif 8 > MONTHS.index(month) > 4:
print('%s: tomatoes' % month)
else:
print('%s: asparagus' % month)
if oysters_good(month):
print('%s: oysters' % month)
elif tomatoes_good(month):
print('%s: tomatoes' % month)
else:
print('%s: asparagus' % month)
if time_for_oysters:
print('%s: oysters' % month)
elif time_for_tomatoes:
print('%s: tomatoes' % month)
else:
print('%s: asparagus' % month)
def tomatoes_good(month):
index = MONTHS.index(month)
return 8 > index > 4
OystersGood('November')
test
test.r
not test.ary
test =
assert
assert
assert
OystersGood('July')
not test
not test.r
not test.ary
Things to remember
Extract variables and functions to
improve readability
Extract variables into classes to
improve testability
Use __bool__ to indicate a class is a
paper trail
>>>
Gregory the Gila
>>>
Gregory the Gila is 3 years old
>>>
Gregory the Gila ate 2 treats
>>>
Gregory the Gila needs a heat lamp? True
Before
class Pet:
def __init__(self, name, age, *,
has_scales=False,
lays_eggs=False,
drinks_milk=False):
self.name = name
self.age = age
self.treats_eaten = 0
self.has_scales = has_scales
self.lays_eggs = lays_eggs
self.drinks_milk = drinks_milk
self.has_scales = has_scales
self.lays_eggs = lays_eggs
self.drinks_milk = drinks_milk
Before
class Pet:
def __init__(self, name, age, *,
has_scales=False,
lays_eggs=False,
drinks_milk=False):
...
>>>
Traceback ...
TypeError: Mixed usage
>>>
UserWarning: Should use Animal
>>>
My pet is Gregory the Gila
>>>
UserWarning: Use animal attribute
Gregory the Gila has scales? True
>>>
Gregory the Gila has scales? True
>>>
Gregory the Gila needs a heat lamp? True
Things to remember
Split classes using optional arguments
to __init__
Use @property to move methods and
fields between classes
Issue warnings in old code paths to
find their occurrences
class Pet:
def __init__(self, name, age, animal):
...
class Pet:
def __init__(self, name, animal):
...
>>>
UserWarning: age
UserWarning: Put
UserWarning: Use
Gregory the Gila
not specified
age on animal
animal.age
is 3 years old
>>>
Gregory the Gila is 3 years old
>>>
AttributeError: can't set attribute
>>>
UserWarning: Assign animal.age
>>>
Gregory the Gila is 5 years old
>>>
Gregory the Gila is 3 years old
>>>
Gregory the Gila is 3 years old
>>>
Traceback ...
AttributeError: Use animal
Things to remember
Use @property.setter to move fields
that can be assigned
Defend against muscle memory with
tombstone @propertys
Follow-up
Links
PMOTW: Warnings - Doug Hellmann
Stop Writing Classes - Jack Diederich
Beyond PEP 8 - Raymond Hettinger
Links
This talk's code & slides:
github.com/bslatkin/pycon2016
My book: EffectivePython.com
Discount today: informit.com/deals
Me: @haxor and onebigfluke.com
Appendix
>>>
Avg: 87.5, Lo: 73.0, Hi: 96.0
Before
def print_stats(grades):
total, count, lo, hi = 0, 0, 100, 0
for grade in grades:
total += grade.score
count += 1
if grade.score < lo:
lo = grade.score
elif grade.score > hi:
hi = grade.score
print('Avg: %f, Lo: %f Hi: %f' %
(total / count, lo, hi))
# Closure
Before
def print_stats(grades):
total, count, lo, hi = 0, 0, 100, 0
for grade in grades:
total += grade.score
count += 1
if grade.score < lo:
lo = grade.score
elif grade.score > hi:
hi = grade.score
print('Avg: %f, Lo: %f Hi: %f' %
(total / count, lo, hi))
# Closure
Things to remember
Extracting a closure function can make
code less clear
Use __call__ to indicate that a class is
just a stateful closure
Closure classes can be tested
independently