Python sequences reference - problem?

Alex Martelli aleax at aleax.it
Thu Sep 19 11:23:36 EDT 2002


Michal Kaukic wrote:

> Hi,
>    
>    we like Python - I use it (with Numeric and SciPy modules)
> for teaching of Numerical analysis and my colleague is doing
> some research in discrete optimization in Python. And he often
> surprises me with non-conformal use of language :-)
> 
>    We are mathematicians, not programmers. I suppose, he believes
> that Python "thinks" like Mathematician. Yesterday, he was struggling

I think the concept of "MODIFYING an object" is pretty far from
most areas of maths I've dabbled in (I'm an engineer, not a
mathematician, but do find maths fascinating).  It seems to me
that the only real way to get a language that WILL "think like
a mathematician" is to take your pick among languages that have
no concept of "modifying an object" - which nowadays basically
means, Functional Programming languages.  Personally I find
Haskell the most elegant one -- clean syntax (uses whitespace
much like Python), clean semantics (lazy evaluation the default
everywhere).  Last I checked, the best Haskell compilers were
able to produce very decent code for many problems -- performance
will in most cases be better than Python's.  If you want even
MORE performance, I suspect some less-puritanical but still FP
language (such as O'Caml) can probably deliver it.

If for some reason you don't want to get into FP, then I think
you can forget everything about "thinking like a mathematician".
If data modification IS allowed, then your chosen language needs 
to choose either of two possibilities:

-- COPY-semantics: an assignment such as
        x = y
   inherently copies y, so that any future change to y has no
   more effect on x

-- REFERENCE-semantics: an assignment such as
        x = y
   makes x a synonym of y, so that both refer to the same
   object: any future change to that object affects any
   synonym for the object equally

Many languages choose a MIX of these two kinds of behavior,
but that's the worst of both worlds, as it complicates things
a lot.  Generally, OLD languages (such as Fortran) relied on
copy semantics (references or synonyms were not considered,
and when they could happen, as in argument-passing, they were
prohibited by the language rules, although those rules were
rarely enforced).  Modern languages by and large have moved
to reference semantics, which is generally much cleaner even
though in some cases performance can suffer (optimization is
the main reason some languages complicate things by mixing
reference and copy semantics).  "Generally" is important: LISP,
one of the oldest languages around (second-oldest after
Fortran, among those still widely used today), relied on
reference semantics from the start.  The resulting elegance
has much to do with Lisp's acceptance in the mathematical
community, IMHO.

Python uses reference semantics.  When you want a copy, you
ask for a copy -- it's as simple as this.  If you don't ask
for a copy, you don't get a copy... you get a reference to
the original.  There is no complication: it's _always_ this
way.  (OK, OK, at the margin there are doubtful cases: in
particular, a SLICE is a copy-request for all built-in types,
but a reference, sharing data with the original array, when
you use the Numeric package... Numeric had to move _away_
from copy and onto reference in this case to avoid accidental
copies of huge array slices... take this as the exception
that proves the rule, OK?-).

Python does have some data types that are immutable, which
helps move it a little bit closer to FP -- numbers, strings
and tuples are immutable.  (Even Java has strings as
immutables -- SOME of FP's niceties do start to move into
the mainstream, albeit slowly:-).  But the main style is still
very much NOT FP, rather relying on modifiable data.

> with the code shown below (somewhat simplified):
>     
> 
> L_L=[[1,2,3], [2,3,4], [3,4,5]]
> 
> def do_process(LL):
>     n=len(LL[0])
>     rec = [0]*n
>     Res=[]
>     
>     for row in LL:
>         rec[0] = row[0]
>         # further code - modifying rec
>         # lots of conditional processing and such...
>         # ...
>         Res += [rec]
>                 
>     return Res
> 
> ---------------------------------------------
> 
> After calling do_process(L_L), the "expected" result should be
> 
>    [[1, 0, 0], [2, 0, 0], [3, 0, 0]]
> 
> but the true result is
> 
>    [[3, 0, 0], [3, 0, 0], [3, 0, 0]].
> 
> 
>    Yes, this is fully in accordance with how the Python language should
> behave:
>        Res += [rec] inserts references to list object rec,
>        which are further modified... (he should use copy(rec) instead).

Right in all respects, yes.

 
>    But there is nothing to make this behaviour clearly VISIBLE in code.

*EVERY* assignment in Python uses reference semantics.  Every occurrence
of an assignment, therefore, makes reference behavior "clearly VISIBLE".

Presumably fish don't find water "clearly VISIBLE", being used to that,
much as it is for us and air.  When something "is *EVERYWHERE*", how
can it be "clearly VISIBLE" at the same time?-)


> If I work with pointers in C/C++ I know and see they are pointers.

That's because C uses copy semantics, and uses pointer to emulate
reference semantics when needed.  That's just the reverse of Python,
which uses reference semantics, and explicit calls to copy to
emulate copy semantics when needed.

C++ lets you define "references" which IMPLY reference semantics
rather than copy semantics -- and you don't SEE what they are at
the point of use (only at the point of definition).  Flaming about
that here, one way or another, won't be very useful:-).  But there
are LOTS of precedents: Pascal (!!!) lets you declare arguments
to a procedure as either reference or copy -- then you don't see
the difference at the point of use; Visual Basic emulated that, too
(the default has changed in recent releases of VB, but that's
-another- flame:-); languages with sophisticated macro systems,
such as Dylan or relatively-recent LISPs, let you do all that and
worse.

But C and Python are SIMPLE languages: one semantics is pervasive
(reference for Python, copy for C), you take explicit measures
when you want the OTHER semantics, and those measures are visible
at the point of use (well, _mostly_ -- C does complicate things
a bit by having arrays "decay to" pointers, Python has the
"slice aren't copies in Numeric" issue -- but that's definitely
at the margin).

> You can say - we also know that rec is list object and so be careful
> with it.  Yes, but consider the complex code where the similar constructs
> are very easy to overlook. And debugging of such code can be frustrating.

The issue has nothing to do with rec being a list object, really.
You may see the issue as having to do with the fact that rec is
(any kind of) *MUTABLE* -- indeed, a language without mutable data
DOES shield you from these issues.  If you tried to mutate non-
mutable data, as rec[0] = .... does, you'd get an exception at
runtime (if every kind data was immutable, of course, the language
wouldn't even have any CONSTRUCTS to express mutation).


>    My colleague was in state of despair and made thoughtful remarks
> about FORTRAN and Python similarity. He believes that Python is corrupting
> his (computer) memory...

Fortran (up to F77 -- I lost touch with it afterwards) had reference 
semantics in all parameter passing to subprograms (with aliasing forbidden 
in rather complex ways -- very few compilers ever enforced that, letting
the hard-to-debug cases for the programmers:-)), copy semantics for all 
assignments.  I can't see any parallel between that and Python's
behavior, not even in the "anti-parallel" sense of Python // C.


>    So what is the point? I wrote this message in hope that there are
> more people with similar experience. My question is - how to explain
> to novice non-programmer users (maybe mathematically "infected")
> the quirks of Python sequence objects? Which methodology to use

First of all, somebody with reasonable math bent will prefer a
more general explanation -- so, forget sequence objects, because
that's just one specific cases.  ALL object-mutation works the
same way, whether it be of a sequence or non-sequence.

> in programs so we can clearly see we work with "pointers"?

You're in water.  Water is *everywhere*.  When you think there
is nothing there, there's water.  Just get used to it.  Trying
to muddy up the water in an attempt to make it more visibile
would be counter-productive.

> Or maybe someone can propose the changes to the Language to overcome
> this (psychological) barrier? I feel this as a serious obstacle
> in using Python (my students experienced it too).

It appears, though you never state it, that the behavior you
would consider "natural" would have to do with copy semantics.
I find that weird, absolutely weird -- I've seen it happen to
old died-in-the-wool programmers without solid mathematical
bases, because for 20 or 30 years of their lives they had only
seen copy semantics and could no longer conceive of any other,
but that's exactly the reverse from the "mathematical bent"
you keep mentioning.

Accepting your mention of "mathematical bent" as relevant, I
repeat my suggestion that you look into functional programming.
FP is seriously cool, IF all the programmers involved have a
solid mathematical outlook on life, the universe and everything.

If for whatever reason you want to keep using languages based
on the idea of data modification, you won't find a simpler or
more regular one than Python.  And it would be absurd to pick
old-enough languages that use copy semantics because some
previous experience (NOT with maths itself, surely?!) makes
you feel that's natural and divergences from it should be
made "very VISIBLE" -- besides Python, you'd be cutting yourself
off from Java, Eiffel, C# ... reference semantics IS the wave
of the last decade-plus, and probably of the next one too
(unless FP comes into its own, but I've sort of stopped holding
my breath for THAT -- there just isn't enough of an audience
with a pervasive-enough mathematical turn of mind:-).

Just remembed that EVERYTHING works by reference, and what
could possibly be the problem...?


Alex




More information about the Python-list mailing list