Python is a very poorly designed language, and when I would sometimes dig to find out why things here how they were, Guido was behind many of the decisions which I believe were bad.
Python is a very poorly designed language, and when I would sometimes dig to find out why things here how they were, Guido was behind many of the decisions which I believe were bad.
I'm definitely not saying python is anywhere near perfect, and yes it certainly has design flaws (as does any complex system). But obviously it's done a lot of things right to gain the reach that it has.
Indeed it has. It is a very accessible (and even moreso "available") language.
But it has so many odd (and frankly wrong) peculiarities that big projects tend to become more difficult more quickly because of the mess that is most Python code. Some of the footguns are so rediculous that you cannot believe they exist; and even more astounding is the apologist arguments for why they should be the way they are. This one is my favorite: https://stackoverflow.com/questions/1132941/least-astonishme...
Since it is so accessible and has such a rich ecosystem, someone with little skill can cobble together a system which works well enough to get to production. But that junior developer pay rate cost (and ease of hiring) will mean that as the project matures and more and more features are added, the code more rapidly becomes incomprehensible. Everything being mutable, no types (except for bolted on, visually noisy, non-runtime linters), OOP missing some of the key information hiding features, awkward or half-baked functional features, and time/space performance problems all combine to make Python a terrible choice for non-prototype level projects.
That’s not a confusing language design. It’s a very simple case of mutability that new programmers stumble over because they don’t yet know how to reason about mutability.
But go ahead, tell us what should have been done instead here? Make it impossible to pass mutable arguments to functions? Or make it impossible to use mutable variables as default?
This should illustrate the problem:
-------------------------------------
Python 3.10.6 (main, Aug 11 2022, 13:36:31) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def visit_city(city_name, visited=[]):
... visited.append(city_name)
... return visited
...
>>> a = visit_city("Boston")
>>> a
['Boston']
>>> a = visit_city("New York", a)
>>> a
['Boston', 'New York']
>>> b = visit_city("Dallas")
>>> b
['Boston', 'New York', 'Dallas']
>>>
--------------------------------
Any reasonable developer would expect b to => ['Dallas']. You absolutely would not expect the value of b to have been influenced by previous calls to visit_city().
Python apologists only offer one positive for this: "It is useful for memoization". That is a very edge case use, and it was not the intention when default arguments were implemented. It was an accidental side use of a misfeature.
No they are not, I’d offer you an example, but you gave one yourself. Are you confused as to what mutable and immutable means?
> You absolutely would not expect the value of b to have been influenced by previous calls to visit_city().
Yes they would. Python is not a functional language. If you have A=[1,2,3] and call A.append(4), what do you expect A to be? Same if you have a function with argument “mylist” and a default set to A, what do you expect to happen to A if you call mylist.append(4) inside that function body on a call that didn’t pass another list?
> Python apologists only offer one positive for this: "It is useful for memoization".
No, that is not the only use, if you couldn’t pass mutable arguments you would cripple the language. And if you couldn’t pass mutable defaults you’d have an odd inconsistent handling of them.
> if you couldn’t pass mutable arguments you would cripple the language
This is not about passing mutable arguments. This is about having a default value for a parameter be mutable (unreliable).
Yes the arguments in Python functions are mutable. That's not unexpected. What is unexpected is that the default value assigned to a non-provided argument is not consistent. In my example I show that the parameter "visited" has a default value of [] if no argument is passed in.
Off the top of my head, Ruby, Javascript, and Swift offer the ability to have default parameter values, and they are all predictably immutable (defaults). Python is the odd one here.
I will say no more about this here, because I'm beating a dead horse.
$a = []
def func(b=$a)
b.append 5
end
func
func
# $a is [5, 5]
They are absolutely not immutable, they just feel immutable. And even then immutable is technically the wrong word — you can obviously mutate params default or not. What you mean is that each invocation gets its own copy of the default but unless you can deep clone arbitrary objects and globals/singletons (i.e. classes) don’t exist this is impossible.Python has actual default values which while sometimes surprising when those values are mutable it is just as much a valid design choice as expressions and both permit mutation.
Default expressions have a different equally crazy footgun side effects.
$a = []
def func(b=$a.append(1))
b.append 2
end
func
func
# $a is [1, 2, 1, 2]Yes, you can stuff anything, even a reference, into the right hand side of a default parameter definition. But in 17 years of using Ruby, I've never done this, never needed to, and never seen anyone else do it. Granted I don't read source of libraries or Rails (except occasionally), so maybe _somewhere_ someone does this. I've read a few Ruby books over the years and never seen this illustrated as a thing to do, even in the Ruby Cookbook book.
(added) To be fair, one thing I have seen which is confusing to people new to programming and applies in Ruby, is if you define a constant EMPTY = [] elsewhere, and you use EMPTY as your default value for a parameter, you will get the same behavior as Python had with just []. That's because EMPTY is a constant reference to an array, but the array is mutable. This would be true for any language which is not immutable. Change the reference? No, that's immutable. Change the contents of the thing it references? Yes.
However, [] would appear to any sane person as an immediate new allocation of an empty array, not an unnamed reference to one spot in memory which gets an empty array allocated for it the first time it is encountered.
In fact, to be very literal, in the Python example the default value is only [] (empty array) once - the first time. In my visit_city() example, after the first run it is now ["some city"]. But if you read that code, and even if you are interactively debugging, the code itself of course does not change. The code still tells you, "Hey, here's a default empty array []". So this utterly fails the principle of least surprise. We expect when we read code that something which appears to be a literal value (or an expression not based on a variable) will evaluate to exactly what it appears to be.
At this point we'll just have to disagree on whether the Python behavior is "sometimes surprising" or "supremely stupid". I would simply put it this way: the use cases for Ruby's (and Swift's and Javascript's and probably any other langauge which offers default function params) fits the more common need, and that Python's opposite behavior is a footgun in a land where people very rarely intentionally want to shoot at feet.
Your final Ruby example is obvious in what it will do, and not shocking at all. And you had to go out of your way to allow that behavior. Nobody does this. No beginner would see this, in a guide or a book. No beginner would accidentally decide to do this.
But beginner and experienced developers alike would come to Python and do def foo(arg=[]):.
No you don’t. And calling a mutable value “not consistent” because it’s not immutable is just changing the words used after your mistake of immutable vs mutable. You haven’t done what you think you’ve done because you don’t understand that [] is not “a new empty list every time it is referenced”, but is just a single mutable list. So you’ve provided a default mutable value, which initially is [], but by the nature of being mutable might change in the future. And that is what gets used if there is no value provided. It’s exactly the same behavior as if you define a named variable with an initially empty list, and pass that list to the default. If you want it to be initiated to [] every time no argument is passed, then you need to initiate a new mutable list as the argument on each call. Just like you wouldn’t be able to define a named list once as empty, and expect every call of .append() to give you a new list with only one element. You’ve passed a single list as the default, not a list imitator, or some crazy “spawns new empty lists on references” object.
And yes, you could have default arguments only support immutable values, then you would not be able to pass in a whole range of objects in defaults.
If you're going to throw around your words so sloppily like this at least give us a few decisions you don't like so we have something real to agree or disagree about, lol
See this thread if you want more than a few examples of how Python is poorly designed (as compared to Ruby): https://news.ycombinator.com/item?id=32116957
IMO the biggest issue is that they add features with absolutely no regard to how you could implement it with good performance. There are also a ton of features that let you do things that are just a really bad idea. Look at something like __subclasses__. Encourages terrible coding practices! (I learnt about it by reading some terrible code my colleagues wrote.) And good luck JITing that.
There's also a ton of syntax that is weird and asymmetric, like list comprehensions. Also the fact that you don't need to explicitly create variables is very error prone. Some of that is excusable given how old Python is but that doesn't mean it isn't bad.
Oh and import resolution. That's just downright terrible. No excuse there. I'd almost take #include over the insane mishmash of different ways imports work. It's even worse than JavaScript's module disaster.
Later ECMAScript editions have made good improvements, but there are still plenty of sharp edges and footguns in Javascript which simply do not need to exist.
Javascript is a leading language because _practically_ speaking (not strictly speaking) it is the only language you can use if you want to make things happen in the browser. And it came with every major web browser since the late 90s. THAT is why it gained immense marketshare.
Python has been included in Linux, *BSD, and macOS for years. It's a very simple language to make things happen with little effort; it is "accessible" to most people, particularly non computer scientists. It reads a little bit more like English than other languages, and it doesn't require compiling (again to accessibility).
All these things don't mean it's good. But it got picked up by non-developers and used to solve real world problems and a lot of research problems. More and more specialized modules were written for it, making it even easier to solve many problems.
So its accessibility and ecosystem have been good. But that doesn't mean it's good for building big, long lived, easily maintainable or extensible systems. It certainly doesn't mean it's good for building performant systems.
See elsewhere in this thread for my link to my biggest complaints (from an old discussion). I'm not a recognized expert on language design, but I've written significant quantities of production code in several languages, including Python, over many years.
No doubt people may disagree with my assertion, but using popularity as evidence of disagreement is not valid since popularity often does not correlate to the quality or suitability of something. History is full of companies, technologies, and practices where the most popular was absolutely not the best.
len() makes no sense for numbers, booleans, None, and probably other types I cannot think of at the moment.
Python has a LONG list of built in functions, most of which are only applicable to a few types of parameters. They could just as well be object methods.
https://docs.python.org/3/library/functions.html
Some of those methods, like sorted(), are terrible names. A past tense verb name... so instead of telling Python, "Hey, please sort() this", you say "Please give me a sorted() thing".
So you have the collection.sort() method or the built in sorted() method. They both sort, but they have different options; and significantly, sort() mutates the collection, whereas sorted() returns a new collection.
This is inconsistent and creates unnecessary cognitive load. It's another example of a Frankenstein language.
There is a method on all objects which are a collection:
__len__()
You can try it out yourself. x = [1,2,3,4]
print(x.__len__()) #4
y = "abcdefg"
print(y.__len__()) #7
z = { "a": "b", "c": "d"}
print(z.__len__()) #2
len() is just syntactic sugar for calling __len__(). The "dunder" methods in python are a standard way of extending classes.It is generally considered bad practice to call dunders, with the idea being that those are more private or more internal methods which you should not directly depend on (unless you're willing to accept more breaking changes). Instead, you depend on other more intentionally public methods which themselves may call dunders or do other things.
I think the issue the parent was really focusing on was the fact that many methods are instance methods, but there are some global functions.
Python suffers from being sort of OO, sort of procedural, and even less but also sort of functional.
It's a bit like a flying camper van. It's a weird plane with tricky controls, poor fuel economy, cramping sleeping areas, and a bump ride on roads. But boy does it have a lot of mods and attachments, so you can do just about anything with it using only a pair of plyers and a flathead screwdriver. Thus, it attracts beginner mechanics and enthusiasts who build monstrosities that experienced mechanics have nightmares about but which pointy haired bosses don't understand and think the feature set is great.
It seems like your inability to understand why a lot of people like python seems more like a professional inability (liability?) to adapt and change with the flow.
Likewise, for example, abs() and __abs__() raise different exceptions if used on something for which abs() makes no sense. Like len(), the built in failure exceptions are somewhat more useful than the dunder exceptions.
But even the type related exceptions of the built in functions are inconsistent -- unnecessarily so.
abs("hello") => TypeError: bad operand type for abs(): 'str'
len(5) => TypeError: object of type 'int' has no len()
At least the exception types are uniform: TypeError. But the exception messages should be identical rather than two very different statements which say the same thing: function f is not appropriate for argument type t.
You may call all this inconsistency freedom, but it's just lack of structure and general sloppiness. Or to be kinder, it's just more organic. Inconsistency and organic evolution are not hallmarks of good tools for building reliable systems.
Finally, I do not lack the inability to understand why a lot of people like Python. I just posit that most people who like Python only like it because they don't (yet) know better. http://www.paulgraham.com/avg.html . Python is Blub in this essay.
If you don't know better, Python is a marvelous language. Although I would argue that some of the oddities and awkwardness of Python misshape peoples' thought processes in a way that can be difficult to unlearn (and which I believe even make these conversations like we are having in this topic extra challenging).
Back to python. There is no type checking in len() or abs(). The best argument I can make is through code. You, yourself, can make your own len() method. This will behave exactly like len(). Let's call it len2():
def len2(obj):
try:
return obj.__len__()
except AttributeError:
raise TypeError(f"object of type '{type(obj)}' has no len()")
There's no type checking, because there's no need. All that object needs is for __len__() to exist. Further we can add abs() and len() to an object like this: class X:
def __len__(self):
return 2
def __abs__(self):
return 1
assert len(X()) == 2
assert len2(X()) == 2
assert abs(X()) == 1
Fortunately you can fix the issues with error uniformity by creating your own functions if you choose the method above. I won't judge you -- because there might be a reason you may want that.> I just posit that most people who like Python only like it because they don't (yet) know better.
I don't know about that. I came from a background of C/C++/Ada/Java/68k Assembly/Perl/Shell Script/Awk/etc. I switched to python about a decade ago. The python community has some of the smartest people I've ever met. To me the smart people seem to have gravitated towards python.
You can either implement a single function that handles the various types or you can add a method to each type, but the latter can be pretty tedious when you're implementing a bunch of functions / methods that operate on numerous types.
I ran into this when I started implementing map() for a few builtin types and found myself duplicating the loop-over-items code and started thinking, "Hmm, I wonder if this is why Python implemented len(), map(), etc as functions instead of methods."
Of course, you could implement an abstract class or equivalent for the generic aspects of len(), map(), etc, but then that gets pretty complex too.
So, based on that, which to reiterate is just a guess, I don't think the decision is bizarre at all. It's just an implementation tradeoff.
From a developer point of view, I slightly prefer method syntax for aesthetic reasons, but practically speaking it doesn't make any difference whether I type obj.len() or len(obj), and IMO neither is more or less object-oriented than the other (which is a common complaint about len() et al).