Experiences/guidance on teaching Python as a first programminglanguage

G

Gene Heskett

This dove-tailer understands Rapid Application Development ....

http://woodwork.ars-informatica.ca/tool.php?art=dovetail_video
Frank Klausz's three-minute dovetails using a bow saw

Frank is a Master, and too many people never really learn to use a bow saw.
However I'd expect that joint would take more glue to fill, I've had a
single drop of TB-III extrude from every edge of one of my jig made joints.

But, I really think we are just a tad off topic.

Cheers, Gene
--
"There are four boxes to be used in defense of liberty:
soap, ballot, jury, and ammo. Please use in that order."
-Ed Howdershelt (Author)
Genes Web page <http://geneslinuxbox.net:6309/gene>

UNIX is hot. It's more than hot. It's steaming. It's quicksilver
lightning with a laserbeam kicker.
-- Michael Jay Tucker
A pen in the hand of this president is far more
dangerous than 200 million guns in the hands of
law-abiding citizens.
 
M

Mark Lawrence

I've always thought C was a great language for low-level, bare-metal,
embedded stuff -- but teaching it to first or second year computer
science students is just insane. C has a certain minimalist
orthogonality that I have always found pleasing. [People who smile
wistfully when they think about the PDP-11 instruction word layouts
probably know what I mean.]

I agree with you here, but wasn't there a tie-in between C and the rise
of Unix via universities, or am I barking in the wrong forest?
But, exposure to C should wait until you have a firm grasp of basic
algorithms and data structures and are proficient in assembly language
for a couple different architectures. Ideally, you should also have
written at least one functioning compiler before learning C as well.

I never had a problem with C as I'd written assembler for RCA 1802,
Ferranti F110L and DEC/VAX, plus CORAL 66. Hum, a bit of a fib there, I
recall vainly struggling with a C for loop before I finally realised I'd
effectively written a CORAL 66 one, page 50 here
http://www.xgc.com/manuals/pdf/xgc-c66-rm.pdf for (ouch!!!) anyone who's
interested. Using a Whitesmith's pre-ANSI C compiler didn't exactly
help me either. IIRC printf was spelt format and all the formatting
codes were different to what became standard C.
 
R

rusi

I can't think of a reference, but I to recall that
bugs-per-line-of-code is nearly constant; it is not language
dependent. So, unscientifically, the more work you can get done
in a line of code, then the fewer bugs you'll have per amount of
work done.

Enter the (One-Liner) Dragon!

 
C

Chris Angelico

Enter the (One-Liner) Dragon!


Some languages work differently with lines, cramming more onto a
single line while still having more "code". What's nearly constant is
bugs per "amount of code", except that it's practically impossible to
measure how much code you've produced. So there are a few exceptions
to the "lines of code" metric, a few languages that jump around a bit
on the scale.

ChrisA
 
J

Joel Goldstick

If its true that bugs per line of code is more or less a constant, I think
the key is that some languages are more expressive than others. So, in
assembler, you are moving data around registers, and doing basic math,
etc. It takes a lot of code to get something done. So maybe more bugs.
Moving up the ladder to C, which is in a way high level assembly language,
you get more done in few lines. Python or other languages maybe do more
per line than C (eg the for loop in python does a lot with very little
code because of python having iterable stuff built in) So, if you have a
language that is expressive and fits your programming needs, you will have
less to debug -- not because you don't make as many errors, but the good
code just does more for you
 
G

Grant Edwards

I've always thought C was a great language for low-level, bare-metal,
embedded stuff -- but teaching it to first or second year computer
science students is just insane. C has a certain minimalist
orthogonality that I have always found pleasing. [People who smile
wistfully when they think about the PDP-11 instruction word layouts
probably know what I mean.]

I agree with you here, but wasn't there a tie-in between C and the rise
of Unix via universities, or am I barking in the wrong forest?

Yes, I think the popularity of Unix on university campuses is what
caused the migration from Pascal to C for freshman programming
classes. IIRC, there were decent Pascal compilers for Unix back then,
so I still think it was a big mistake. Later on when studying low
level OS stuff would have been a fine time to introduce C if required
for logistical reasons.
 
R

Roy Smith

Wolfgang Keller said:
C is just a kafkaesque mess invented by a sadistic pervert who must
have regularly consumed illegal substances for breakfast.

Don't be absurd. C is a perfectly good language for the kinds of things
it's meant for. It lets you get down close to the hardware while still
using rational flow control and program structure, and being reasonably
portable.

There's very few mysteries in C. You never have to wonder what the
lifetime of an object is, or be mystified by which of the 7 signatures
of Foo.foo() are going to get called, or just what operation "x + y" is
actually going to perform.

If you maim yourself with a razor-sharp chisel, do you blame the chisel
for being a bad tool?
 
R

Roy Smith

Grant Edwards said:
Ideally, you should also have written at least one functioning
compiler before learning C as well.

Why? I've never written a compiler. I've written plenty of C. I don't
see how my lack of compiler writing experience has hindered my ability
to write C.
 
D

Dennis Lee Bieber

It wasn't for me either when I went to college in the late 1970's.
Pascal first, then FORTRAN, then IBM 360 assembler. That was all the
formal language training I had. (I had taught myself BASIC in high
school.)

Pascal was an elective at my school -- small class size having to share
a pair of CRDS LSI-11s running UCSD Pascal.

The CompSci core curriculum had parallel threads of

(intro) FORTRAN, (adv) FORTRAN, Sigma-6 Assembler (Meta-Symbol)
(intro) COBOL, (adv) COBOL, Database (DBTG Network model)

The OS course also used the CRDS LSI-11 and Macro-11 (which was taught
in the first few sessions, one was expected to be familiar with assembly
language programming on the Sigma)

I took APL as independent study just to get the last three (only needed
two) credits for graduation.

The algorithms class was a bit of a free-for-all; we where permitted to
use any language the instructor could make sense of -- which meant FORTRAN,
COBOL, Pascal, BASIC, and Assembly; SNOBOL and APL were not viable. I ended
up using BASIC -- on a system that only allowed four open files at a time
-- and I used program chaining to switch among the primary functions of the
assignment (a hashed head multiple linked list "phone directory"). I don't
even want to imagine the effort it took in Assembler (memory claims someone
DID choose that implementation)
 
S

Steven D'Aprano

There's very few mysteries in C.

Apart from "What the hell does this piece of code actually do?". It's no
coincidence that C, and Perl which borrows a lot of syntax from C, are
the two champion languages for writing obfuscated code.

And "What does 'implementation-specific undefined behaviour' actually
mean in practice?", another common question when dealing with C.

And most importantly, "how many asterisks do I need, and where do I put
them?" (only half joking).

You never have to wonder what the
lifetime of an object is,

Since C isn't object oriented, the lifetime of objects in C is, um, any
number you like. "The lifetime of objects in <some language with no
objects> is ONE MILLION YEARS!!!" is as good as any other vacuously true
statement.

or be mystified by which of the 7 signatures
of Foo.foo() are going to get called,

Is that even possible in C? If Foo is a struct, and Foo.foo a member, I
don't think C has first-class functions and so Foo.foo can't be callable.
But if I'm wrong, and it is callable, then surely with no arguments there
can only be one signature that Foo.foo() might call, even if C supported
generic functions, which I don't believe it does.

(You can simulate something rather like generic functions using pointers,
but that's it.)

or just what operation "x + y" is
actually going to perform.


With no operator overloading, that one at least is correct.
 
C

Chris Angelico

Apart from "What the hell does this piece of code actually do?". It's no
coincidence that C, and Perl which borrows a lot of syntax from C, are
the two champion languages for writing obfuscated code.

I thought APL would beat both of them, though you're right that the
International Obfuscoted Python Code Contest would be a quite
different beast. But maybe it'd be just as viable... a competent
programmer can write unreadable code in any language.
And "What does 'implementation-specific undefined behaviour' actually
mean in practice?", another common question when dealing with C.

You mean like mutating locals()? The only difference is that there are
a lot more implementations of C than there are of Python (especially
popular and well-used implementations). There are plenty of things you
shouldn't do in Python, but instead of calling them
"implementation-specific undefined behaviour", we call them
"consenting adults" and "shooting yourself in the foot".
And most importantly, "how many asterisks do I need, and where do I put
them?" (only half joking).

The one differentiation that I don't like is between the . and ->
operators. The distinction feels like syntactic salt. There's no
context when both are valid, save in C++ where you can create a
"pointer-like object" that implements the -> operator (and has the .
operator for its own members).
Since C isn't object oriented, the lifetime of objects in C is, um, any
number you like. "The lifetime of objects in <some language with no
objects> is ONE MILLION YEARS!!!" is as good as any other vacuously true
statement.

Lifetime still matters. The difference between automatic and static
variables is lifetime - you come back into this function and the same
value is there waiting for you. Call it "values" or "things" instead
of "objects" if it makes you feel better, but the consideration is
identical. (And in C++, it becomes critical, with object destructors
being used to release resources. So you need to know.)
Is that even possible in C? If Foo is a struct, and Foo.foo a member, I
don't think C has first-class functions and so Foo.foo can't be callable.
But if I'm wrong, and it is callable, then surely with no arguments there
can only be one signature that Foo.foo() might call, even if C supported
generic functions, which I don't believe it does.

Well, okay. In C you can't have Foo.foo(). But if that were Foo_foo(),
then the point would be better made, because C will have just one
function of that name (barring preprocessor shenanigans, of course).
In C++, the types of its arguments may affect which function is called
(polymorphism), and the dot notation works, too; but C++ does this
without having first-class functions, so that part of your response is
immaterial. In C++, Foo.foo() will always call a function foo defined
in the class of which Foo is an instance. Very simple, and static type
analysis will tell you exactly which function that is. Things do get a
bit messier with pointers, because a function might be virtual or not
virtual; C++ gives us the simple option (non-virtual functions) that
most high level languages don't (C++'s virtual functions behave the
same way as Python member functions do).

ChrisA
 
D

Devin Jeanpierre

There's very few mysteries in C. You never have to wonder what the
lifetime of an object is

Yes you do. Lifetimes are hard, because you need to malloc a lot, and
there is no defined lifetime for pointers -- they could last for just
the lifetime of a stack frame, or until the end of the program, or
anywhere in-between, and it's impossible to know for sure, and if you
get it wrong your program crashes. So there's all these conventions
you have to come up with like "borrowing" and "owning", but they
aren't compiler-enforced, so you still have to figure it out, and you
will get it wrong. Successors like C++ mitigate these issues with
destructors (allowing heap-allocated stuff to be tied to the lifetime
of a stack), and smart pointers and so on.
, or be mystified by which of the 7 signatures
of Foo.foo() are going to get called

C still has overloaded functions, just fewer of them. It'll still
mystify you when you encounter it, though.
http://www.robertgamble.net/2012/01/c11-generic-selections.html
, or just what operation "x + y" is
actually going to perform.

I don't know. Will it do float addition? int addition? size_t
addition? How does coercion work?

+ can do many different things, it's not just a straight translation
to an obvious machine instruction.
If you maim yourself with a razor-sharp chisel, do you blame the chisel
for being a bad tool?

If I didn't need it to be that sharp, then yes.

-- Devin
 
C

Chris Angelico

Yes you do. Lifetimes are hard, because you need to malloc a lot, and
there is no defined lifetime for pointers -- they could last for just
the lifetime of a stack frame, or until the end of the program, or
anywhere in-between, and it's impossible to know for sure, and if you
get it wrong your program crashes. So there's all these conventions
you have to come up with like "borrowing" and "owning", but they
aren't compiler-enforced, so you still have to figure it out, and you
will get it wrong. Successors like C++ mitigate these issues with
destructors (allowing heap-allocated stuff to be tied to the lifetime
of a stack), and smart pointers and so on.

Wrong. A pointer is a scalar value, usually some kind of integer, and
its lifetime is the same as any other scalar. Heap memory's lifetime
is also very simple: it lasts until freed. (Though technically that's
not even a part of the language - malloc/free are just functions. Not
that it matters. Anyway, C++ has the new and delete operators, which
are part of the language.) There are conventions to prevent memory
leaks, but those are mere conventions. It's simple in the same way
that a toy electric motor is simple: you apply current to it, and it
spins. There's so little that it can do that it HAS to be simple.

ChrisA
 
D

Devin Jeanpierre

Wrong. A pointer is a scalar value, usually some kind of integer, and
its lifetime is the same as any other scalar.

The duration of a pointer's validity is far more interesting, and that
is why it is the primary meaning of the term "pointer lifetime". Also,
it's obviously what I meant.
Heap memory's lifetime
is also very simple: it lasts until freed.

Sometimes simple things are hard to use correctly. I only said it was
hard, not complicated.

-- Devin
 
C

Chris Angelico

The duration of a pointer's validity is far more interesting, and that
is why it is the primary meaning of the term "pointer lifetime". Also,
it's obviously what I meant.

Sometimes simple things are hard to use correctly. I only said it was
hard, not complicated.

Sure, which is why I went on to discuss the block of memory pointed
to. But the rules are a lot simpler than in Python, where something
exists until... uhh... the system feels like disposing of it. At which
point __del__ will probably be called, but we can't be sure of that.
All we know about an object's lifetime in Python is that it will
continue to live so long as we're using it. And then multiprocessing
and fork make it messier, but that's true in any language.

The original point was that C has no mysteries. I posit that this is
true because C's rules are so simple. It might well be harder to work
in this system (taking it to an extreme, Brainf* is about the simplest
Turing-complete language possible, and it's virtually impossible to
write good code in it), but it has no mysteries.

ChrisA
 
P

Paul Smith

And "What does 'implementation-specific undefined behaviour' actually
mean in practice?", another common question when dealing with C.

Only asked by people who haven't had it explained. There's "undefined
behavior", and there's "implementation-specific behavior", but it is
impossible to have "implementation-specific undefined behavior".

And, the definitions are simple to understand: "undefined behavior"
means that if your program invokes it, there is no definition of what
will happen. This is buggy code.

"Implementation-specific" behavior means that the standard requires the
implementation to do some well-defined thing, but the standard does not
define exactly what it must be. You can go look up what your
implementation will do in its documentation (the standard requires that
it be documented), but you can't assume the same thing will happen in
another implementation. This is non-portable code.

It's a very rare language indeed that has no undefined or
implementation-specific behaviors. Python gets to "cheat" by having one
reference implementation. Every time you've had to go try something out
in the Python interpreter because the documentation didn't provide the
details you needed, that WAS implementation-specific behavior.
Since C isn't object oriented, the lifetime of objects in C is, um, any
number you like. "The lifetime of objects in <some language with no
objects> is ONE MILLION YEARS!!!" is as good as any other vacuously true
statement.

The implication that only an "object oriented" language could have a
concept of object lifetimes is false. Another, less hyperbolic way of
saying this is that in C, the lifetime of objects is _exactly as long as
you specify_. Heap objects come into existence when you explicitly
create them, and they go out of existence when you explicitly destroy
them. If you don't destroy them, they never go away. If you destroy
them more than once, that's undefined behavior. Stack objects are even
simpler.
Is that even possible in C? If Foo is a struct, and Foo.foo a member, I
don't think C has first-class functions and so Foo.foo can't be callable.

Of course that's valid C. It's true that C doesn't have first-class
functions, but it supports invoking functions through pointers and you
can store functions in data members, pass functions as arguments, and
return functions from other functions, so Foo.foo can certainly be
callable.

~$ cat /tmp/foo.c
#include <stdio.h>

struct Foo {
void (*foo)();
};

void foobar(void) { printf("foobar\n"); }

int main()
{
struct Foo Foo = { foobar };
Foo.foo();
return 0;
}

$ gcc -Wall -o /tmp/foo /tmp/foo.c

$ /tmp/foo
foobar
 
S

Steven D'Aprano

With no operator overloading, that one at least is correct.


Actually, I stand corrected. I was completely mistaken about that. The C
operation x + y is undefined if the addition overflows. A valid C
compiler can produce whatever code it damn well feels like in the case of
overflow.

Oh, and in case you think that integer overflow in C will always follow
two's complement semantics, such that INT_MAX+1 = INT_MIN, you are wrong.
That's not guaranteed either. Clang and gcc have a flag, -fwrapv, to
force defined behaviour on integer overflow, but that's not part of the C
standard and not all C compilers will do the same.
 
S

Steven D'Aprano

Only asked by people who haven't had it explained. There's "undefined
behavior", and there's "implementation-specific behavior", but it is
impossible to have "implementation-specific undefined behavior".

Of course it is possible. An implementation has to do *something*, even
if it's not defined anywhere. Even if that "something" is crash, or emit
no code, or display a compile-time error. I think you're making a
distinction that doesn't apply to the plain-English meaning of the words
I was using: the behaviour is undefined by the standard and specific to
that implementation.

And, the definitions are simple to understand: "undefined behavior"
means that if your program invokes it, there is no definition of what
will happen. This is buggy code.

Yes, it is buggy code, but nevertheless it often works the way people
expect it to work, and so through carelessness or ignorance programmers
rely on it. If you've ever written i+1 without a guard for the case that
i is INT_MAX, you're guilty of that too.

The C99 standard lists 191 different kinds of undefined behavior,
including what happens when there is an unmatched ' or " on a line of
source code.

You want to know why programs written in C are so often full of security
holes? One reason is "undefined behaviour". The C language doesn't give a
damn about writing *correct* code, it only cares about writing
*efficient* code. Consequently, one little error, and does the compiler
tell you that you've done something undefined? No, it turns your validator
into a no-op -- silently:

http://code.google.com/p/nativeclient/issues/detail?id=245

No compile-time error, no run-time error, just blindingly fast and
correct (according to the standard) code that does the wrong thing.

"Implementation-specific" behavior means that the standard requires the
implementation to do some well-defined thing, but the standard does not
define exactly what it must be. You can go look up what your
implementation will do in its documentation (the standard requires that
it be documented), but you can't assume the same thing will happen in
another implementation. This is non-portable code.

So much for the promise of C to be portable :)

It's a very rare language indeed that has no undefined or
implementation-specific behaviors.

Java? Ada?

But indeed, most languages do have odd corners where odd things happen.
Including Python, as you point out. But C has so many of them, and they
affect *nearly everything*.

The aim of C is to write fast code, and if it happens to be correct,
that's a bonus. C compilers will compromise on safety and correctness in
order to be fast. Then end result is usually one of two outcomes:

- the programmer spends a lot of time and effort to manually guard
against the undefined behaviour, thus slowing down the code;

- or he doesn't, and has bugs and security vulnerabilities in the code.

Python gets to "cheat" by having one
reference implementation. Every time you've had to go try something out
in the Python interpreter because the documentation didn't provide the
details you needed, that WAS implementation-specific behavior.

The situation is quite different though. Python makes at least one
implicit promise: nothing you write in pure Python can possibly cause a
segfault. No buffer overflows for you!

We don't know what locals()['spam'] = 42 will do inside a function, but
unlike the C case, we can reason about it:

- it may bind 42 to the name "spam";

- it may raise a runtime exception;

- it may even be a no-op;

But even if it is a no-op, the Python compiler doesn't have carte blanche
to do anything it likes with the entire function, as a C compiler has. C
has more indeterminacy than Python.


The implication that only an "object oriented" language could have a
concept of object lifetimes is false.

Only object-oriented languages have *objects*. C does not have objects,
it has values.

And yes, I'm being pedantic.


[...]
Of course that's valid C. It's true that C doesn't have first-class
functions, but it supports invoking functions through pointers and you
can store functions in data members, pass functions as arguments, and
return functions from other functions, so Foo.foo can certainly be
callable.

Okay, fair enough, that's why I prefixed my statement with a question.
 
S

Steven D'Aprano

I thought APL would beat both of them, though you're right that the
International Obfuscoted Python Code Contest would be a quite different
beast. But maybe it'd be just as viable... a competent programmer can
write unreadable code in any language.


You mean like mutating locals()? The only difference is that there are a
lot more implementations of C than there are of Python (especially
popular and well-used implementations). There are plenty of things you
shouldn't do in Python, but instead of calling them
"implementation-specific undefined behaviour", we call them "consenting
adults" and "shooting yourself in the foot".


The one differentiation that I don't like is between the . and ->
operators. The distinction feels like syntactic salt. There's no context
when both are valid, save in C++ where you can create a "pointer-like
object" that implements the -> operator (and has the . operator for its
own members).


Lifetime still matters. The difference between automatic and static
variables is lifetime - you come back into this function and the same
value is there waiting for you. Call it "values" or "things" instead of
"objects" if it makes you feel better, but the consideration is
identical. (And in C++, it becomes critical, with object destructors
being used to release resources. So you need to know.)


Well, okay. In C you can't have Foo.foo().

Hah, well according to Paul Smith's example code you can. So either:


- it's possible to be an experienced C programmer and still have
fundamental gaps in your knowledge about basic concepts like dotted
function calls;

- or Paul's sample code was not what he claimed it to be;

- or maybe the whole thing is undefined and we're all right! C both does
and doesn't allow Foo.foo() function calls, *sometimes at the same time*.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,768
Messages
2,569,575
Members
45,053
Latest member
billing-software

Latest Threads

Top