Think Like
a Programmer
Paradigms For Program Design
Solve easy problems
Defer hard ones till they are easy
-- James Howison [1]
Program Layout
Create A Simple Logical Flow
Identify Use Cases
Problem Description:
[Some process] data is monitored by different sensors. Data collected from sensors stored in CSV files. Want to collect all the data in a single Google Sheet for graph rendering and other post-processing. Data points will include timestamps and will be in sequential order. Only keep data columns that correspond to headers specified in the Google sheet.
Use Cases:
Refined Use Cases:
Define Goal
Split Into Small, Easy Pieces
Goal:
Manage Time Series Data
Functionality:
Design - Load & Save
def __init__(self, fp=None, ts_col=None):
self.rows = []
self.fp = fp
self.ts_col = ts_col
self.headers = None
self.rows_loaded = 0
def __str__(self):
return f'<{__class__.__name__}: [rows={len(self)}]>'
def __len__(self):
return len(self.rows)
def load(self):
# get rows from self.fp
# get headers from first row
# convert ts_col into datetime.datetime
# append to self.rows
# save rows_loaded
def save(self):
# write rows to self.fp
Design - Append new data
def append(self, rows):
# use bisect to find index of timestamps
# that are newer than existing data
# append new data to self.rows
Design - Specify Headers
load():
append():
Constructor:
No need to write new methods, just modify the following ...
Encapsulation
Isolate Unrelated Concerns
def __init__( self, fp=None, … ):
self.fp = fp
def load(self):
# get rows from self.fp
def save(self):
# write rows to self.fp
For an easier append …
def __add__( self, other ):
# use bisect to find index of timestamps
# that are newer than existing data
# append new data to self.rows
Hide Changing Things
def __len__(self): …
Hides a changing thing …
… from an unchanging thing
Use "property" to hide get & set methods
Manual Getters and Setters:
class aThing:
def __init__(self):
self._x = …
def getx(self):
return self._x
def setx(self, val):
self._x = val
myThing = aThing()
myThing.setx("a new value")
print( f'x is {myThing.getx}' )
Property
class aThing:
def __init__(self):
self._x = …
def getx(self):
return self._x
def setx(self, val):
self._x = val
x = property( getx, setx )
myThing = aThing()
myThing.x = "a new value"
print( f'x is {myThing.x}' )
getters
setters
clunky?
better!
"Property" as a decorator
See also: https://docs.python.org/3/library/functions.html?highlight=property#property
class aThing:
def __init__(self):
self._x = …
@property
def x(self):
return self._x
@x.setter
def x(self, val):
self._x = val
myThing = aThing()
myThing.x = "a new value"
print( f'x is {myThing.x}' )
Now they all match!!
Environment Variables
Use ChainMap to prioritize program options
import os, argparse
defaults = {
'color': 'red',
'user': 'guest
}
def process_cmdline():
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
cmdline_args = {
k: v
for k, v in vars(namespace).items()
if v is not None
}
return cmdline_args
combined = ChainMap(
process_cmdline(),
os.environ,
defaults
)
print(combined['color'])
print(combined['user'])
1st
Last
2nd
For more ideas on line length:
https://treyhunner.com/2017/07/craft-your-python-like-poetry/
Low Barrier to Entry
Nothing beats a runnable sample
A runnable sample provides a starting point to explore
Key Points
One command if possible …
curl <URL>/quickstart.sh | bash
Runnable Sample in a single command line
URL=https://github.com/USER/PROJECT.git
die() {
# Print msg to stdout and exit
}
tmpdir=$(mktemp -d)
which git &>- || die "Git not found"
git ls-remote "$URL" &>- || die "Bad URL '$URL'"
git clone "$URL" $tmpdir/repo
$tmpdir/repo/sample_run.sh
rm -rf $tmpdir
}
quickstart.sh
# Check python version **
# Setup
python -m venv venv
venv/bin/pip install --upgrade -r requirements.txt
# Sample run
export USER=${USER:-guest}
export COLOR=${COLOR:-red}
python sample_run.py
sample_run.sh
References