2013-09-01
I started reading Mark Summerfield's new book Python in Practice. The first
section covers object-oriented software design patterns. Everytime I come across
an article or a book that goes over patterns I tell myself I need to make a point
of illustrating these from my own experiences, as I can never remember all their
names. Here's an example of the prototype
pattern from a recent bit of code.
I find the term prototype
a bit misleading. What this pattern really
represents is cloning
- making an copy of an object. The python standard
library comes with the copy module that provides a couple of methods
for doing just that. My example does not use the copy module however.
This blog is made with my own homegrown static site generator. The one problem I had was how to encapsulate a path in the site's file structure so that I could easily transform it from a source file (e.g. a markdown file) to its final location in the build directory (e.g. as an html file).
A source file would look something like the following, and I need to discern both the root directory, the relative site directory path as well as the file name and it's possible extension:
/home/freddie/blog/contents/blog/posts/prototype_pattern.md
\_________________________/\_________/\________________/\___/
| | | |
root dir base ext
Now I want to make a copy my path, but replace any of the parts - most
importantly the root
and the ext
.
/home/freddie/blog/build/blog/posts/prototype_pattern.html
\______________________/\_________/\________________/\___/
| | | |
root dir base ext
The basic path object looks like this:
class PathInfo(object):
def __init__(self, root, dir_, base, ext=None):
self.root = root
self.dir_ = dir_
# split the ext from base if it has one
basename, ext_ = os.path.splitext(base)
self.base = basename
self.ext = ext or ext_
self.rel_path = join('/', self.dir_.strip('/'), self.base + self.ext)
self.full_path = join(self.root, self.dir_.strip('/'), self.base + self.ext)
self.full_dir = os.path.dirname(self.full_path)
I could have implemented the rel_path
, full_path
and full_dir
attributes
as methods, but decided is was better to deal with setting their values immediately.
The only wonky bit is the ext
keyword argument. When fetching
directory information from os.walk
a file's extension won't be separate and there
is no real need to separate it out prior to initializing a PathInfo
object,
however, in order to be able to transform the file's extension this keyword argument
is required.
Making a copy of the PathInfo
just involves initializing a new instance with the
originating instance's values. That is the essential idea of the prototype pattern -
we're just cloning the original object. In addition I am providing the ability to
transform the cloned version:
def copy_as(self, **kwargs):
root = kwargs.get('root', self.root)
dir_ = kwargs.get('dir', self.dir_)
base = kwargs.get('base', self.base)
ext = kwargs.get('ext', self.ext)
return PathInfo(root, dir_, base, ext)
Let's say we want to copy a directory of files to another:
def copy_dir(src_dir, dst_dir):
import shutil
if not os.path.exists(src_dir) or not os.path.exists(dst_dir):
raise Exception('The source or destination directory does not exist.')
# collect the source directory path info
src_paths = []
for root, dirs, files in os.walk(src_dir):
relpath = os.path.relpath(root, src_dir)
relpath = "" if relpath == "." else relpath
for file_name in files:
# here's our original prototype
src_paths.append(PathInfo(src_dir, relpath, file_name))
# copy and transform our paths with a new root
for src in src_paths:
# here's our clone and transform
dst = src.copy_as(root=dst_dir)
if not os.path.exists(dst.full_dir):
os.makedirs(dst.full_dir)
shutil.copy(src.full_path, dst.full_path)