1 of 19

Think Like

a Programmer

Paradigms For Program Design

2 of 19

Solve easy problems

Defer hard ones till they are easy

-- James Howison [1]

3 of 19

Program Layout

Create A Simple Logical Flow

  1. Identify use cases
  2. Define goal (from use cases)
  3. Split into small, easy pieces
  4. Design one piece at a time

4 of 19

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:

  • Read data from a CSV-file.
  • Write data to a CSV-file.
  • Read data from Google Sheets.
  • Write data to Google Sheets.
  • Append new data, ignoring duplicates.
  • Save only user-specified columns.

Refined Use Cases:

  • Use a Python "file object"

  • Append new data, ignoring duplicates.
  • Save only user-specified columns.

5 of 19

Define Goal

Split Into Small, Easy Pieces

Goal:

Manage Time Series Data

Functionality:

  • Read data from a "file object"
  • Write data to a "file object"
  • Append new data
  • Specify column headers

6 of 19

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

7 of 19

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

8 of 19

Design - Specify Headers

load():

  • If self.headers already defined:
    • remove columns not specified in self.headers

append():

  • Skip columns not specified in self.headers

Constructor:

  • Add "headers" to initial constructor

No need to write new methods, just modify the following ...

9 of 19

Encapsulation

  • Isolate unrelated concerns
  • Hide changing things

10 of 19

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

  • We don't know, or care, how data is stored.
  • We just expect a known interface.

For an easier append …

  • Define how to add two objects together

def __add__( self, other ):

# use bisect to find index of timestamps

# that are newer than existing data

# append new data to self.rows

11 of 19

Hide Changing Things

def __len__(self): …

Hides a changing thing …

  • self.rows

… from an unchanging thing

  • external interface (to get the length)

12 of 19

Use "property" to hide get & set methods

Manual Getters and Setters:

  • Require unique names
  • Don't look like attribute accesses

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

  • Hide get & set methods
  • Look like attribute accesses

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!

13 of 19

"Property" as a decorator

  • Functions use the attribute name
  • Easier to read
  • Easier to search
    • looking for attribute x
    • find functions named as such

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!!

14 of 19

Environment Variables

  • Increase control options
  • Expected in containers

15 of 19

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

16 of 19

Low Barrier to Entry

Nothing beats a runnable sample

  • Keep it short
  • Clean up after running
  • Idempotent

17 of 19

A runnable sample provides a starting point to explore

Key Points

  • Keep it SHORT
    • One command if possible
  • Clean up after running
    • Don't muck up a customer's home
  • Idempotent

One command if possible …

curl <URL>/quickstart.sh | bash

18 of 19

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

19 of 19

References

  1. Howison, J., & Crowston, K. (2014). Collaboration through open superposition: A theory of the open source way. MIS Quarterly, 38(1), 29–50. DOI: 10.25300/MISQ/2014/38.1.02