It's lack of a normal, standard scoping model alone teaches a very flawed reasoning about computer programs.
There are many languages which hammer worse practices than either such as POSIX shell scripts, but they are seldom used as a teaching language.
Python is in the unique space of having made horrible design decisions but somehow often used as a teaching language.
One would assume that programmers are to understand such concepts as block scoping rules and the difference between lexical and dynamic variables. — how is Python to teach them that?
IMO the ideal programming track would be as follows:
Intro to programming track:
* Assembly --> to understand the basics of how computers work and learn simple procedural and abstraction rules.
* C --> more procedural code, more abstractions, and higher order memory management
* Java --> to learn interfaces, object oriented code, and an introduction to functional programming concepts like lambdas, pure functions, the value of immutable objects
* {Racket/LISP or Favorite functional language} --> to gain intuition with more pure functional programming
Then, I'd have separate tools track:
* web dev
* database
* embedded dev
* AI, etc.
Then a separate theory track:
* context free languages, regular expressions, compiler design
* algorithms and data structures
* some network theory and fault tolerance results
Then I'd have a professional track:
* tools: learn source control systems, IDEs, debuggers, profilers, bug management systems, different dev systems, using Jenkins or other CIT tools.
* skills: learn about secure coding (best practices for writing auditable code with known failure modes), efficient coding (calculating big O for a given code, fixing performance bottlenecks), maintainable coding (best practices for decoupling code, writing testable code, writing unit tests)
* theory: track where you learn advanced algorithm theory, design patterns, hopefully centered around case studies.
With elective tracks for things like OS theory, networking, etc.
Too many people leave college with lots of information about data structures and algorithms but don't know how to write a unit test, how to use an IDE, how to write unit tests, how to write secure, maintainable code.
I don't think it's really practical for every scenario. I still think it's a great tool for teaching students about many aspects of how a modern computer, or operating-system functions. You definitely won't get far in gaining a holistic understanding of any modern operating-system without understanding C.
Jesus Christ, Java as an introduction to functional programming? I don't even know what to say.
Scoping in class definitions is weird, yes. But is that your complaint? What's wrong with the scoping model in general?
That for-loops work by assignment rather than creating a new scope, or that if-conditions do not create a new scope for their arms is most unusual.
Not only does it not teach programmers to properly reason about scope, but it results into subtle bugs that are easy to miss. Consider the following:
list = []
for x in iterator:
list.append(lambda y: some_code_that_closes_over(x))
This almost certainly does not behave as the programmer intended, for the for-loop does not create a new scope, so all iterations of the loop share the same scope, and thus every closure that closes over `x` handles the same `x`, which will have the value that `x` had at the last loop, thus effectively mutating the closure after appending it to the list, so that the list will only contain effectively identical closures.The proper way do it is by using the fact that functions do create a scope:
list = []
def loop_function(x):
list.append(lambda y: some_code_that_coses_over(x))
for x in iterator:
loop_function(x)
Certainly code that looks quite hackey to work around the lack of normal, expected block scoping. list.append(functools.partial(some_code_that_closes_over, x))
If you replace a lambda with a function definition in the for loop, it's easier to see the scoping issue is actually a closure. And that makes sense because lambda is shorthand for a function.Apart from that, your code technically does not do the same thing as it calls the inner function with two arguments, whereas mine simply discards the second argument.
Maybe your complaint is about the fact that statements inside a lambda isn't called until the lambda is called. But again, lambda's are just functions, and closures are closures.
Or maybe your argument is that closures are easy to make in python, which is true. But that doesn't mean the language is bad, since lots of languages use closures.
x = something
# some lines of code
for x in iterator:
# something
# try to use x again here
# x has been re-assigned by the loop
In about any other language, the loop would create it's own scope, and thus shadow the existence of the outer `x`, rather than re-assigning it.But even so, this is a lousy example since x is already defined at the top. So the for loop could theoretically use the defined x already.
I think what you're looking for is something akin to the following in C:
int x = 5;
for (int x=0; x<10; x++) {
int y = x;
}
But I would argue that this is terrible C code since the inner x shadows the outer scope leading to confusion as to which x the loop is iterating on. But you could also write the for loop like this: for (x<0; x<10; x++) { ... }
So C doesn't dictate the scope of the loop which could be argued by your standards, even more confusing than python. Python just says your scope is just function level. Subtle C code changes can create scoping bugs.So even the scoping example your trying to use is fraught with issues for new developers. Is "function-level" scoping better than "C-style" scoping say? I tend to think that function level scoping forces developers to simplify their code as it discourages symbolic shadows.
In your example, the variable `x` is also shared with all iterations of the loop. Rather, it is more so as so in C, like syntax what is the common approach:
while(1) {
int x = next(iterator)
if(STOPITER) { break }
/* code that uses x */
}
Every iteration of the loop receives a brand new `x` rather than re-assigning the old `x`, this problem is illustrated by creating a closure that closes over the `x`, for which the expected behavior is not that the `x` is then assigned to another value on the next iteration of the loop.The only way to achieve this in Python is to create an ad-hoc function which is passed `x` as a formal parameter, for every new function call in Python does create a new scope rather than simply re-assigning to the last one.
In that case I would prefer to be explicit that the closure "wraps" a new instance of the variable rather than relying upon implicit language behavior to guarantee the scope is correct.
As python says in "import this": "Explicit is better than implicit."
Then you have argued that using any form of functions or subroutines in any language is bad design.
> since now you're constantly asking "When does a variable leave scope." If I change the scope of a variable, it causes a subtle change that breaks the closure.
Yes, that is what one must ask oneself as a programmer and that is what programmers who have not been taught wrong practices by having used Python as their introduction are instinctively constantly asking themselves.
Python programmers must also ask themselves this whenever they use functions and Python comes with a variety of ugly hacks around it's initial wanton design by a programmer who clearly does not understand scope how he originally designed it such as `nonlocal`.
Inside any function in Python there are four kinds of variables in terms of their scope: normal, formal, global, and nonlocal, each of them has a different scope and the programmer best be mindful of which is which as he codes, because they have widely different semantics and because Python has the same syntax for assignment and initialization and this difference especially creates very different interpretations for those four types.
> In that case I would prefer to be explicit that the closure "wraps" a new instance of the variable rather than relying upon implicit language behavior to guarantee the scope is correct.
I feel that you have a fundamental misunderstanding of what a closure or function is if you think it even possible for it to wrap a new instance of a variable.
You seem to be of the same misunderstanding as another user above that `let x = x;`-esque behavior would affect this issue in any way rather than be an irrelevant line.
> As python says in "import this": "Explicit is better than implicit."
Not only is this very rich coming from a language that uses exceptions for flow control, and the same syntax for initialization and assignment, it's also irrelevant and not a matter of explicitness versus implicitness.
Whether wrapping variables would be implicit or explicit in this context would not have solved the issue at all. Even if Python mandated explicit wrapping or made shadowing illegal, which some languages do, for which there is argument to be had, it would not change this subtle bug at all.
(And if we broaden our view to include more of software engineering in general, like solving the right problems and validating hypotheses with fast feedback, it seems even more insignificant.)
Python code seems to either be written from a mentality of not at all caring about variable lifetime and making it far longer than it should be, or programmers that do care, and uses classes and functions as makeshift tools to attempt to limit the lifetime of variables to make up for the lack of block scope of the language.
list = []
for x in iterator:
list.append(lambda y, x=x: some_code_that_closes_over(x))
That way the closure is explicit and much clearer when you look at it later.("list" is a builtin, so not a good variable name.)
Your code is illegal Python on two levels.
- Lambdas in Python do not allow assignment at all.
- Variables in Python cannot be assigned using themselves in their r.h.s., without first being assigned something else, because Python's scoping is strange.
You serve well as an example of a programmer that does not understand Python's semantics here, because they are very counter-intuitive, and you also serve as an example of a programmer that does not understand how scope works at all, even if your example be worked into something of valid Python syntax:
thunks = []
for x in "abcd":
def thunk():
y = x
print(y, end='')
thunks.append(thunk)
for thunk in thunks:
thunk()
The output is `dddd`, not `abcd`; the `y = x` part is completely irrelevant in this case, for when the thunk is called, `x` has already been re-assigned, and so `y = x`, is assigned the new value.Again, `x` is re-assigned on every new iteration of the loop, as such the `x` in every single one of those thunks contains the value `x` had at the last iteration of the loop when the loop completes.
There are many ways to solve this issue, such as the one I initially gave, but yours isn't one of them and that you thought it was shows the counter-intuitive nature of Python's behavior here.
> ("list" is a builtin, so not a good variable name.)
Which would be another problem with Python's lack of scope. Shadowing the names of library functions and constants is not problematic in languages with proper block scope.
Yours:
In [1]: list = []
...: for x in range(5):
...: list.append(lambda y: y+x)
...: [f(100) for f in list]
Out[1]: [104, 104, 104, 104, 104]
Mine: In [2]: list = []
...: for x in range(5):
...: list.append(lambda y, x=x: y+x)
...: [f(100) for f in list]
Out[2]: [100, 101, 102, 103, 104]
I understand the semantics just fine, and it's very clear what's going on. In my version, each lambda has two parameters, one called y, and one called x. The one called x has a default value, and the default value is whatever the loop variable x's current value is: a copy is made of the loop variable's value, and stored as a default parameter value. There is no assignment, and a variable is not assigned to itself. The parameter has the same name, but the scope is different; within the lambda, the parameter shadows external variables, just as in any function.This is idiomatic Python code, taught to beginners (search for n=n - it's about this same loop thing): https://realpython.com/python-lambda/
You do the same thing when passing parameters through to an inner function, like this:
def outer(x, y):
def inner(z, x=x):
return z+x
print(inner(2*y))
Shadowing list in a function scope is fine (just confusing), but your code does it in the global scope.You seem to have a bone to pick with Python for some reason, but to me your attempts at criticism fall completely flat, as they are either factually incorrect or amount to "Python is different from [language x]".
lambda x: x = x; do_thing(x)
Rather than the: lambda x, x = x: do_thing(x)
And yes, your example works, and is functionally similar to my initial solution of using that function scope does exist by simply creating a function for no other reason than to call it to create a new scope.That doesn't stop that both examples are ugly hacks needed to solve a problem with the language that most languages do not have. And despite your calling it idiomatic, I have never seen your example in the wild and it is, frankly, simply a hack that few programmers that haven't been explicitly taught this trick would quickly see the intend behind.