[conspire] Why I loves me the Python.

Nick Moffitt nick at zork.net
Mon Sep 21 16:56:44 PDT 2009


Rick Moen:
> > [1] There is actually a "rational subset"[2] of perl that's actually
> > quite readable.
> 
> Yes, indeed.  Nick Moffitt (also on this mailing list) was actually the
> first Perlista whose code I could consistently read, because he writes
> C-like Perl, bless him.  Before I saw his scripting, I was worried I'd
> never learn to understand the damned stuff.

Eep!  You're showing off the coding equivalent of embarrassing disco
outfit photos here :)

I haven't written a lick of Perl in nearly a decade, now (I think my
last Perl was for a consulting gig doing GNU Bayonne scripting).  About
ten years ago I spent a while trying to wrap my head around why Python
was a worthwhile language to code in, and that simmered on the back
burner while I did ridiculous things in GNU make[1] and awk[2] and such.
I tend to work almost exclusively in bash, AWK, and Python these days.

I'm not impressed by much of the "line noise" snark at Perl, since I
think that most of that originally came from people who had never worked
with regular expressions before.  The symbolic decoration on variable
types is something that probably seemed less odd to me because of my
experience with 6502 and 8086 assemblers that used # and $ for somewhat
similar things.  Still, I can see how some folks see the punctuation as
helpful visual landmarks while others see it as indication of
complexity.  It's definitely an emotional reaction more than anything.

I do appreciate the sentiment that a disciplined or naturally clear
programmer can write good code in nearly any language[3], but I find
that this little bromide often gets trotted out to silence critical
analysis of a language's flaws.  It's not interesting to know that good
programmers can do good things with poor tools.  The fact that Mozart
could write grand opera in German doesn't necessarily make it the ideal
language for the task.

I tend to prickle up at the Python party line that once you get used to
the invisible syntax, you'll believe it to be an asset.  I find myself
emotionally drifting toward this position, but intellectually I still
feel like it's more true to say that Python's success *despite* this
wart shows off its other strengths incredibly well.  Discussions of how
Python encourages you to be a good coder often get dragged down into
bikeshedding about a syntactic feature that I'm still not convinced is
as helpful as they say.

This distracting debate causes people to ignore a few things that  I
love most about the language:

	1: Namespaces.

		Namespaces can feel like esoteric constraints in other
		languages, like you're just artificially walling off
		symbols from each other in confusing and arbitrary ways.
		Java seems to force lots of them on you, and early
		jabs at Java were all about the
		very.long.paths.through.modules.to.functions().

		By contrast, Python namespaces are a simple mapping onto
		the filesystem, at the root, and actual data structures
		in your own code toward the leaves.  If most other
		language implementations just adopted Python's namespace
		handling and import syntax, I'd give them a closer look.

		Net effect: if you have gronk/spoot/fooble.py (with
		__init__.py files in the gronk and spoot dirs), and say
		a hork object with a zorch method inside fooble.py, you
		could call it in python by doing:

			import gronk
			gronk.spoot.fooble.hork.zorch()

		-or-

			from gronk.spoot import fooble as wut
			wut.hork.zorch()

		-or-

			from gronk.spoot.fooble import *
			hork.zorch()

		...and so on.  I'm amazed that people manage to code
		anything useful at all in PHP, with a polluted root
		namespace like this:

			http://www.php.net/quickref.php 

		It's like keeping all your files in / without any
		subdirectories!  They adopted underscore conventions,
		but that doesn't get you "relative path" operations.  I
		hear newer PHP releases have namespaces, but it sounds
		like it will be ages before they can reorganize their
		standard library.

	2: Generators/Iterators.

		A common pattern in programming is to generate a list of
		items in memory, and then iterate over that list to
		perform some operation on each element within.  The
		trouble with this is that if your list is large and you
		don't need to keep it around for anything but the walk
		straight down it, you're pretty much wasting time and
		memory building it up in the first place.

		Python gives you a great tool for making your functions
		return something that is as good as a list for
		iteration's sake, but which lazily only creates the next
		item when it's asked for.  

		Consider the old way:

			def stuff():
			    i = 0
			    l = []
			    while not_done_yet():
			        l.append(get_a_thing(i))
				i += 1
			    return l

			for thing in stuff():
			    print thing
		
		If not_done_yet() doesn't return False until you've done
		zillions of get_a_thing() calls, this would fail at user
		interaction.  You'd wait for hours, maybe, and then your
		screen would whoosh by with far too much text to deal
		with.

		So we change it all:

			def stuff():
			    i = 0
			    while not_done_yet():
			        yield get_a_thing(i)
				i += 1

			for thing in stuff():
			    print thing

		Because our function uses "yield" instead of "return",
		it immediately returns a funky "generator object" that
		the for loop can iterate over.  With each iteration the
		yield basically pauses stuff() and "returns" the value,
		and on the next iteration it un-pauses and picks up
		right on the next line after the yield.

		I avoided this feature for a long time after it entered
		the language because it seemed really mysterious (after
		all, it's kind of like scheme's call/cc), but boy am I
		glad I went back to look into it!  I've found that
		inserting this sort of lazy evaluation into my code in
		specific circumstances has been a fantastic way to
		improve performance and responsiveness--often with a net
		reduction in code length (as in the above admittedly
		pathological example).

	3: Duck Typing Gone Mad

		Python's a dynamically-typed language, which basically
		just means you don't have a type checker scolding you
		for writing a for loop and not caring if the thing
		you're iterating over is a proper list or if it's just a
		funky generator function that uses "yield".  The basic
		type system is often called "duck typing" after the old
		saw about something that looks like a duck and quacks
		like a duck.

		This sounds like an invitation to chaos, and I think
		programming language theorists are still slightly
		baffled at why it isn't a catastrophic failure.  One
		interesting trend is that some of the sorts of things we
		used to rely on type checking for are now handled by
		unit testing.  But things work well in Python because
		if you implement some basic methods in your classes,
		your objects get to take advantage of core language
		syntax.

		So if you implement the necessary method to become a
		"callable", hey presto your object can behave like a
		simple function.  If you implement the __getitem__()
		method, you can do "x = myobject[key]" just as if it
		were a dictionary!  You can pretend to be a string, an
		integer, or any number of core data types.  If you want
		to get really crazy you can use this stuff to
		dynamically present namespaces to other code.

		So this takes the straitjacket off the 'EVERYTHING IS AN
		OBJECT' philosophy.  Sure everything's an object, but
		objects can also trivially become anything.
	
	4: Really Powerful Function Arguments

		Man, I really love named function arguments.

			def foo(thingy, length=20, width=42):
			   ...

			squarethingy = foo(mythingy, width=20)

		The above just accepted the default value for length,
		which is 20.  The call is also clearer, you're passing
		in a thingy which is obvious, but passing in lists of
		integers gets eye-glazing after a while.  This saves you
		the step of digging through for the definition of the
		function to see whether width or length came first in
		the argument list.

		But variable arguments are also great:

			def bar(thingy, *args, **kwargs):

		In this case thingy is the first parameter, then "args"
		is a read-only list of all the arguments that were
		passed in without the =, and then kwargs is a
		*dictionary* of the foo=bar arguments that were passed
		in, so:

			bar(mything, 1, 2, 3, 4, buckle="my shoe",
				shut="the door")

		...would call bar with thingy set to mything, args would
		be the tuple (1, 2, 3, 4) and kwargs would be a
		dictionary where kwargs['buckle'] is "my shoe" and
		kwargs['shut'] is "the door".

		For bonus points, you can reverse it:

			y = baz(somefink, *my_list, **a_nice_dictionary)

		...and that will unpack the list and put each element
		into the argument list, and then unpack all the
		dictionary entries to form keyword arguments.

		I've kind of spazzed all over, explaining this, but it's
		just an example of how something as simple as namespaces
		or passing arguments to functions can be designed the
		right way and provide you lots of opportunities to write
		clear and useful code.

Tying most of them together, namespaces are largely implemented as (or
at least presented as) ordinary dictionaries[4]!  So thanks to duck
typing, obj.member is usually the same as obj.__dict__['member'],
similar to the way keyword arguments and dictionaries can swap back and
forth. 

...Of course all of these things would be the usual sort of useless
gilding on esoteric never-see-production-use languages if it weren't for
the fact that the Python developers went to the trouble of actually
integrating smoothly with OS services, made the namespaces map onto
grubby filesystem details, and spent time writing libraries of neat
stuff similar to Perl's collection.

Does this make Python magically spew forth good code from bad
programmers?  No, in the same way that Perl can't completely knock the
legs out from under good programmers.  I find that one distressing sign
of a bad interface between Python and a C library of some sort will be
a scarcity of duck typing tricks: you may find yourself with a bunch of
"get_first_foo()" and "get_next_foo()" functions instead of an object
that implements the methods necessary to behave as a Python sequence or
iterator.  

Python's "list comprehension" syntax, while it's a useful shortcut,
really looks more like the haskell it derived from than true Python.
Likewise, that yield thing can really confuse someone who isn't familiar
with it, and I'll admit that I am responsible for some flow-control head
scratching now and again.  Still, good programmers can write good code
in any language, isn't that right brainfu--I mean, isn't that right
folks?


-- 
"Here is the memo if you didn't receive it: GNOME and
Free Software is all about *SAVING THE WORLD* not
drawing pissy little buttons on the screen."
                -- Jeff Waugh

1: http://en.wikipedia.org/wiki/GARNOME
2: http://www.firthworks.com/roger/cloak/ (search for AWK)
3: That said, ain't nobody can make an asterisk config I can follow
	without headaches the next morning.
4: This can get wasteful, so you can use the 'slots' system to fix a
	class's member variables in a tuple, which *really* helps the
	JIT accelerators like psyco!
5: Yeah look at that Date header.  I should get to sleep.




More information about the conspire mailing list