Monday, January 9, 2017

The Depths of Degradation or How to Reduce

Let's talk real-world functional programming. Disclosure: I'm a fan of functional programming in Python. (This: https://www.packtpub.com/application-development/functional-python-programming)

The usual culprits for functional programming are map(), filter(), generator functions, and the various comprehensions. This is very pleasant and can lead to succinct, expressive code.

The reduce operation, however, is sometimes slippery.  The obvious reductions are sum() and prod().  Some slightly less obvious reductions are these three:

sum0 = lambda s: sum(1 for _ in s)
sum1 = lambda s: sum(s)
sum2 = lambda s: sum(n**2 for n in s)

The first is essentially len(s), but stated more formally. It shows how we can add in filter or transformations. If we're working with a collections.Counter object, we can rewrite these three to work with the values() of a counter. This allows us to have a statistics library that works with a sequence of simple items or a Counter of binned items.

(I've left it as an exercise for the reader to create the summaries of Counters.)

The Health Check Question

The context is an RESTful application's /health end-point. When a client does a GET to /health, we want to provide status of the components on which the app depends as well as a summary.

The details are created like this:

components = (component() for component in COMPONENT_LIST)
init_components = [thing.init_app(app) for thing in components]
details = [component.health() for component in init_components]

We have a list of class definitions for each component. We can create instances of each class. We can initialize these by providing the RESTful app. Finally, we can create a list of the various health end-point status codes.

There's a class definition for other RESTful API's. The health check does a transitive GET to a /health end-point. These are all more-or-less identical.

There are also class definitions for the database and the cache and other non-RESTful components. It's all very pretty and very functional.

Note that the three statements aren't adjacent. They're scattered around to fit better with the way Flask works. The component list is in one place. The initialization happens before the first request. The details are computed as requested.

Also. We don't really use a simple list for the details. It's actually a mapping from which we will derive a vector. I've left that detail out because it's a relatively simple complication.

Representation of Health

We represent health with a simple enumeration of values:

from enum import Enum
class Status(Enum):
    OK = "OK"
    DEGRADED = "DEGRADED"
    DOWN = "DOWN"

This provides the essential definition of health for our purposes. We don't drag around details of the degradation; that's something that we have to determine by looking at our consoles and logs and stuff.  Degradation is (a) rare, and (b) nuanced. Some degradations are mere annoyances: one of the servers is being restarted. Other degradations are hints that something else might be going on that needs investigation: database primary server is down and we're running on a secondary.

Summarizing Health

A subset of the details vector, then, looks like this: [Status.OK, Status.OK, Status.DEGRADED].

How can we summarize this?

First, we need some rules.  Like these:

class Status(Enum):
    OK = "OK"
    DEGRADED = "DEGRADED"
    DOWN = "DOWN"

    def depth(self, other):
        if self == self.OK:
            return {self.OK: self.OK,
                    self.DEGRADED: self.DEGRADED,
                    self.DOWN: self.DEGRADED}[other]
        elif self == self.DEGRADED:
            return {self.OK: self.DEGRADED,
                    self.DEGRADED: self.DEGRADED,
                    self.DOWN: self.DEGRADED}[other]
        elif self == self.DOWN:
            return {self.OK: self.DEGRADED,
                    self.DEGRADED: self.DEGRADED,
                    self.DOWN: self.DOWN}[other]


The depth() method implements a comparison operator that defines the relationships. This can be visualized as a table.

depthOKDEGRADEDDOWN
OKOKDEGRADEDDOWN
DEGRADEDDEGRADEDDEGRADEDDEGRADED
DOWNDOWNDEGRADED DOWN


This allows us to define a function that uses reduce to summarize the vector of status values.

from functools import reduce
def summary(sequence): 
    return reduce(lambda a, b: a.depth(b), sequence)

The reduce() function applies a binary operator between items in a vector. We've used lambda a, b: a.depth(b) to turn the the depth() method into a binary operator so it can be used with reduce.

The summary() function is a "depth-reduction" of a vector of status objects. It's defined independently of the actual status objects. The relationships among the status levels are embedded in the class definition where they belong. The actual details of status are pleasantly opaque.

And.

We have an example of map-reduce outside the sphere of big data.

The Integer Alternative

The health rules as shown above are kind of complex. Could they be simplified? The answer is no.

Here's an alternative -- which does not do what we want.

class Status2(IntEnum):
    OK = 1
    DEGRADED = 2
    DOWN = 3
    
summary2 = lambda sequence: max(sequence)

This works in some cases, but it doesn't work in others. Another alternative is to change the order to be OK=1, DOWN=2, DEGRADED=3. This doesn't work, either. I'll leave it as an exercise to write out some of the various combinations of values and see how these differ.

JSON Representation

The final detail is JSONification of the status vector and the summary.

json.dumps({"status": summary(vector).name, "details": [s.name for s in vector]})

This converts the various Status objects to text items that fit the Swagger specification for our /health end-points. The .name attribute reference is required to get the string labels from the enum. An alternative is to customize the JSON encoder to recognize the Enum objects and extract their names.

Conclusion

Map-Reduce is easy. It surfaces in a number of places. The idea helps encapsulate summarization rules.

Tuesday, January 3, 2017

The "Build Script" Idea

In compiled languages, the build script or makefile is pretty important. Java has maven (and gradle and ant) for this job.

Python doesn't really have much for this. Mostly because it's needless.

However.

Some folks like the idea of a build script. I've been asked for suggestions.

First and foremost: Go Slow. A build script is not essential. It's barely even helpful. Python isn't Java. There's no maven/gradle/ant nonsense because it isn't necessary. Make is a poor choice of tools for reasons we'll see below.

For folks new to Python, here's the step that's sometimes important.

python setup.py sdist bdist_wheel upload

This uses the source distribution tools (sdist) to build a "wheel" out of the source code. That's the only thing that's important, and even that's optional. The source is all that really exists, and a Git Pull is the only thing that's truly required.

Really. There's no compilation, and there's no reason to do any processing prior to uploading source.

For folks experienced with Python, this may be obvious. For folks not so experienced, it's difficult to emphasize enough that Python is just source. No "class" files. No "jar" files. No "war" files. No "ear" files. None of that. A wheel is a Zip archive that follows some simple conventions.

Some Preliminary Steps

A modicum of care is a good idea before simply uploading something. There are a few steps that make some sense.

  1. Run pylint to check for obvious code problems. A low pylint score indicates that the code needs to be cleaned up. There's no magically ideal number, but with a few judicious "disable" comments, it's easy to get to 10.00.
  2. Run mypy to check the type hints. If mypy complains, you've got potentially serious problems.
  3. Run py.test and get a coverage report. There's no magically perfect test coverage number: more is better. Even 100% line-of-code coverage doesn't necessarily mean that all of the potential combinations of logic paths have been covered.
  4. Run sphinx to create documentation.
Only py.test has a simple pass-fail aspect. If the unit tests don't pass: that's a clear problem. 

The Script

Using make doesn't work out terribly well. It can be used, but it seems to me to be too confusing to set up properly.

Why? Because we don't have the kind of simple file relationships with which make works out so nicely. If we had simple *.c -> *.o -> *.ar kinds of relationships, make would be perfect. We don't have that, and this seems to make make more trouble than it's worth.  Both pylint and py.test keep history as well as produce reports. Sphinx is make-like already, which is why I'm leery of layering on the complexity.

My preference is something like this:

import pytest
from pylint import epylint as lint
import sphinx
from mypy.api import api

(pylint_stdout, pylint_stderr) = lint.py_run('*.py', return_std=True)
print(pylint_stdout.getvalue())

result = mypy.api.run('*.py')

pytest.main(["futurize_both/tests"])

sphinx.main(['source', 'build/html', '-b', 'singlehtml'])

The point here is to simply run the four tools and then look at the output to see what needs to be fixed. Circumstances will dictate changes to the parameters being used. New features will need different reports than bug fixes. Some parts of a project will have different focus than other parts. Conversion from Python 2 to Python 3 will indicate a shift in focus, also.

The idea of a one-size-fits-all script seems inappropriate. These tools are sophisticated. Each has a distinctive feature set. Tweaking the parameters by editing the build script seems like a simple, flexible solution. I'm not comfortable defining parameter-parsing options for this, since each project I work on seems to be unique.

Important. Right now, mypy-lang in the PyPI repository and mypy in GitHub differ. The GitHub version includes an api module; the PyPI release does not include this. This script may not work for you, depending on which mypy release you're using. This will change in the future, making things nicer. Until then, you may want to run mypy "the hard way" using subprocess.check_call().

In enterprise software development environments, it can make sense to set some thresholds for pylint and pytest coverage. It is very helpful to include type hints everywhere, also. In this context, it might make sense to parse the output from lint, mypy, and py.test to stop processing if some quality thresholds are met.

As noted above: Go Slow. This kind of tool automation isn't required and might actually be harmful if done badly. Arguing over pylint metrics isn't as helpful as writing unit test cases. I worry about teams developing an inappropriate focus on pylint or coverage reports -- and the associated numerology -- to the exclusion of sensible automated testing.

I think tools like https://pypi.python.org/pypi/pytest-bdd might be of more value than a simplistic "automated" tool chain. Automation doesn't seem as helpful as clarity in test design. I like the BDD idea with Gherkin test specifications because the Given-When-Then story outline seems to be very helpful for test design.

Tuesday, December 20, 2016

The Royal Road

Warning: Long Boring Anecdote: Conclusions will be drawn from a single example.

First, this quote:
"All your suggestions were great if I had wanted to do a systematic study and truly understand. The goal is to understand and learn as little as possible to be able to undertake the code challenge for [a specific job opportunity]."
The subject of systematic study is Python. The focus of learn as little as possible is Pandas.

The goal is more-or-less impossible. Focus on a specific code challenge will devolve into other aspects of the language. Or. It will lead nowhere.

Also. I'm not sure what a "systematic study" is. From the omitted back-story, I'm seems clear to me that the advice to "read a tutorial" is restated here as "systematic study." And is unacceptable. I guess because of time constraints.

It gets better.

There's this:
"For now, I am just going to follow the pattern and get stuff to work."
This is the ideal way to be defeated by technology. The "pattern" is defined to a large degree by the programming language. The two are inextricably linked. Trying to identify a pattern in programming that's magically not part of the implementation language seems deranged.

Learning Pandas isn't simple. There's no royal road.

The real crux are several questions that are difficult to reproduce, so I'm forced to summarize.

  • What does name.name().name().name() mean? How can you call multiple methods "simultaneously"?
  • What is the range of values for some parameter? 

And the capper.

  • I understand object.method().method().  I cannot understand object.name -- how can a method have no ()'s?
I'm sorry, but, the advice still stands. These are not questions that can be answered in a vacuum. This is serious -- and foundational -- object-oriented programming. Each of these small things was a total show-stopper, leading to four emails merely to clarify the question. Then more when the answer was rejected as not consistent with something, or astonishing, or 

For the name.name().name(), the answer "Google Fluent Interface" was too complex. Code examples were requested to show how it was possible for an object to return another object that had methods. 

Advice to use Python's >>> prompt and the dir() function were apparently part of "systematic study" and not part of "learn as little as possible."

For the range of values of a parameter, the answer "read the source" was a non-starter. Reading the source was flat-out rejected as not making any sense. I finally had to actually provide the link to the source repo for Pandas before it became clear what "open source" even meant. This was found to be nothing short of astonishing. The side-bar conversation on "how is this even possible?" was confusing to me because -- sadly -- I assumed people knew that the words "open" and "source" together meant that the source was open for inspection. My assumption was wrong. At least one person did not know this. That means there are others.

Finally. For the "capper" question. The exchange really did include this: "The dot notation I thought was Object.Method.Method."

A great deal of the back-and-forth amounted to "I reject anything other than fluent methods because that's the only thing I've decided to understand." Words like "property" and "attribute" were ignored as noise, AFAIK.

I say "amounted to" because a lot of the back-and-forth was restating the question. Other parts were exchanging links to the Pandas documentation in an effort to follow the "learn as little as possible" strategy. Any link to a tutorial would be "systematic study". Any link to Pandas, however, was acceptable. But (of course) confusing because the Pandas documentation assumes a modicum of language knowledge.

Here's what appears to be the problem: it's impossible to learn a complex tool like Pandas without starting with a basic understanding of Python. 

I don't think I've ever seen it suggested that one can leap into a package without knowing the language. I'm not sure how one can develop the idea of learning as little as possible in the first place. But, there it is. 

There's at least one person who thinks they can learn as little as possible and still get Pandas code to work. That likely means there are others.

It appears there's a market for books like 

Learn as little Python as possible to be able to use Pandas

and

Learn the least Java necessary to make Spark work

and

The Royal Road to Data Science

I'm not sure I'm capable of writing books like these, but for someone who does, it might be a really lucrative line of books.

Tuesday, December 13, 2016

Amazing how Windows is “special.”

Here's the quote:
...it is amazing how Windows is “special.” Back when ..., special things had to be done for Windows. Python continues the tradition w/ an entire section in its doco titled “3. Using Python on Windows”
I wasn't sure what to make of this.

It appeared that they want Windows to be the norm and Linux/Mac OS/POSIX to be treated as an exception.

I find that baffling. POSIX is a standard. Mac OS X is POSIX-compliant. Linux distros are generally POSIX-compliant. There's a nice list: https://en.wikipedia.org/wiki/POSIX

Windows is not POSIX-compliant. It seems to me that non-standard == special shouldn't be "amazing." It should be "tiresome" or "annoying."

Windows tools are uniquely awful. Or, perhaps, "special". Windows is so "special" that the IDE concept appears to have evolved as a solution to the awfulness. Using a Windows IDE (like Visual Studio) insulated one from the vagaries of Windows. It appears this is particularly important when trying to create a binary that will work in multiple incarnations of Windows.

In Linux (and POSIX-compliant OS's in general) the OS is the IDE. Start here: https://sanctum.geek.nz/arabesque/unix-as-ide-introduction/.  This seems so much simpler and more rational. Perhaps I'm just biased because I've used so many OS's that aren't Windows.

Worth considering: http://www.psychocats.net/ubuntu/virtualbox

When asked about IDE's for Python, I tell people that I've used a number of text editors to write Python code:
  • vi
  • BBEdit
  • Atom
  • Komodo Edit
  • Notepad++
  • PyCharm
  • IDLE
  • Jupyter Notebook
They all work nicely. It's difficult to recommend one because they all have distinct features. I always wind up with a lot of command-line interaction. The "run-a-command-from-the-IDE" has complex dialog boxes and sometimes confusing limitations. It's easier to simply write a script than discern the nuances of the IDE configuration rules.

These are (mostly) platform-independent. They can minimize a few of the Windows "features." They don't eliminate all of the Windows issues.

In all cases -- except using IDLE -- I also have a Python `>>>` prompt open in a terminal window.  

I strongly encourage everyone to work this way. The terminal window interaction can be copied and pasted into doctest strings. You've written a unit test without really trying. It's extremely productive. It gets away from IDE wrappers. It does expose some Windows-isms, but as long as you can limit the number of times you find Windows "amazing," that's not a problem.

Tuesday, November 29, 2016

A Reason for Avoiding Programming

From someone in the process of becoming a data scientist. They had a question on regular expressions, which made almost no sense. It appears that the core concepts of ETL -- Extracting source data, Transforming it into a useful form and the Loading into some persistent storage for long-term analysis -- had not been embraced. It appears the design pattern was unknown. All I could gather from the sketchy email chain was that something involving regular expressions had become difficult.

I wrote this in response: Handling Irregular File Formats.

Here's part of the follow-up.

"I have been focusing on the math associated w/ math optimization. I have been using spreadsheets to perform the computations."

Really.

Spreadsheets.

The ETL pipeline question/rant/complaint was part of loading a spreadsheet?

That seems somehow wrong. There are real tools available that really do real data science work. The word "optimization" hints that scipy.optimize might be a more useful exercise than hacking around with spreadsheets.

Perhaps some advice from a real data scientist might help: http://www.becomingadatascientist.com

Tuesday, November 22, 2016

The Modern Python Cookbook

See https://www.packtpub.com/application-development/modern-python-cookbook

This is a large (!) collection of recipes, focused on Python 3, exclusively.

It's much easier to write about the version of Python I actually use each day, and leave the old, quirky, slow Python 2 behind. This book doesn't have any "this will be different in Python 2" warnings. Those days seem to have passed. Finally.

The clock is counting down. https://pythonclock.org


Wednesday, November 16, 2016