Help me pick an API design (OO vs functional)

Steven D'Aprano steve+comp.lang.python at pearwood.info
Thu Mar 28 01:42:35 CET 2013


On Wed, 27 Mar 2013 02:34:09 -0700, Michael Herrmann wrote:

> On Tuesday, March 26, 2013 11:37:23 PM UTC+1, Steven D'Aprano wrote:
>> 
>> Global *variables* are bad, not global functions. You have one global
>> variable, "the current window". So long as your API makes it obvious
>> when the current window changes, implicitly operating on the current
>> window is no more dangerous than Python's implicit operations on the
>> current namespace (e.g. "x = 2" binds 2 to x in the current namespace).
> 
> I'm generally wary of everything global, but you're right as long as no
> (global) state is involved.

That comment surprises me. Your preferred API:

switch_to(notepad)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')


uses implied global state (the current window). Even if you avoid the use 
of an actual global for (say) an instance attribute, it's still 
semantically a global. Surely you realise that?

Not trying to be argumentative, I'm just surprised at your comment.


>> I recommend you look at the random.py API. You have a Random class,
>> that allows the user to generate as many independent random number
>> generators as needed. And the module also initialises a private
>> instance, and exposes the methods of that instance as top-level
>> functions, to cover the 90% simple case where your application only
>> cares about a single RNG.
> 
> I looked it up - I think this is a very good approach; to provide easy
> access to the functionality used in 90% of cases but still give users
> the flexibility to cover the edge cases.
> 
> After everybody's input, I think Design #2 or Design #4 would be the
> best fit for us:
> 
> Design #2:
>         notepad_1 = start("Notepad")
>         notepad_2 = start("Notepad")
>         switch_to(notepad_1)
>         write("Hello World!")
>         press(CTRL + 'a', CTRL + 'c')
>         switch_to(notepad_2)
>         press(CTRL + 'v')


This is nice syntax for trivial cases and beginners whose needs are not 
demanding, but annoying for experts who have more complicated 
requirements. If this is the only API, experts who need to simultaneously 
operate in two windows will be forced to write unproductive boilerplate 
code that does nothing but jump from window to window.

Well what do you know, even in the simple case above, you have 
unproductive code that does nothing but jump from window to window :-)

I'm not against this API, I'm just against it as the *only* API.


> Design #4:
>         notepad_1 = start("Notepad")
>         notepad_2 = start("Notepad")
>         notepad_1.activate()
>         write("Hello World!")
>         press(CTRL + 'a', CTRL + 'c')
>         notepad_2.activate()
>         press(CTRL + 'v')

This is actually no different from #2 above, except that it uses method 
call syntax while #2 uses function call syntax. So it has the same 
limitations as above: it's simple for simple uses, but annoying for 
complex use.

Neither API supports advanced users with complicated needs. A hybrid 
approach, where you have function call syntax that operates on the 
implicit current window, plus method call syntax that operates on any 
window, strikes me as the best of both worlds. With a little forethought 
in your implementation, you don't have to duplicate code. E.g. something 
like this:


class WindowOps:
    def __init__(self, theWindow=None):
        self.theWindow = None

    def press(self, c):
        win = self.getWindow()
        send_keypress_to(win)

    def getWindow(self):
        if self.theWindow is None:
            return gTheTopWindow
        return self.theWindow


_implicit = WindowOps(None)

press = _implicit.press
# etc.

del _implicit



This gives you the best of both worlds, for free: a simple API using an 
implicit top window for simple cases, and a slightly more complex API 
with an explicit window for advanced users.



> Normally, I'd go for Design #4, as it results in one less global,

I don't see how this is possible. Both APIs use an implicit "top window". 
What's the one less global you are referring to?


> is
> better for autocompletion etc. The thing with our library is that it
> tries to make its scripts as similar as possible to giving instructions
> to someone looking over their shoulder at a screen. And in this
> situation you would just say
> 
>        activate(notepad)
> 
> rather than
> 
>        notepad.activate().

Depends like Yoda they talk whether or not.

Unless you go all the way to writing your own parser that accepts English-
like syntax, like Hypertalk:

select notepad
type hello world

I don't think it makes that much difference. Function call syntax is not 
exactly English-like either. We don't generally speak like this:

write bracket quote hello world quote close bracket


My personal feeling is that people aren't going to be *too* confused by 
method call syntax, especially not if they've seen or been introduced to 
any programming at all. You say tom-a-to, I say tom-ar-to.

But I think it is useful to distinguish between the "basic API" using 
function call syntax and an implied current window, and an "advanced API" 
using method call syntax with an explicit window:

# Basic API is pure function calls, using an implicit window
switch_to(notepad)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
switch_to(calculator)
write('2+3=')
press(CTRL + 'a')
switch_to(notepad)
press(CTRL + 'v')


# Advanced API uses an explicit window and method calls:
notepad.write("Hello World!")
notepad.press(CTRL + 'a', CTRL + 'c')
calculator.write('2+3=')
calculator.press(CTRL + 'a')
notepad.press(CTRL + 'v')


# Of course you can mix usage:
switch_to(notepad)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
calculator.write('2+3=')
calculator.press(CTRL + 'a')
press(CTRL + 'v')


You could avoid method call syntax altogether by giving your functions an 
optional argument that points to the window to operate on:

write("Hello World!")
write("Hello World!", notepad)

but the difference is mere syntax.

There's little or no additional complexity of implementation to allow the 
user to optionally specify an explicit window. I expect you have 
something like this:

def write(string):
    window = gTheCurrentWindow  # use a global
    send characters of string to window  # magic goes here


This can trivially be changed to:


def write(string, window=None):
    if window is None:
        window = gTheCurrentWindow
    send characters of string to window  # magic goes here


See, for example, the decimal module. Most operations take an optional 
"context" argument that specifies the number of decimal places, rounding 
mode, etc. If not supplied, the global "current context" is used.
This gives the simplicity and convenience of a global, without the 
disadvantages.

(Recent versions of decimal have also added "with context" syntax, for 
even more power.)




-- 
Steven



More information about the Python-list mailing list