This would be horrendously inefficient without immutable data structures like Clojure's. Very few languages have that, so it's a strange assumption to make, especially for a language as old as Python.
Although it is a very nice feature of Clojure.
“persistent vectors” are certainly an interesting data structure that strike a compromise between fast indexing and being able to relatively quickly create a copy where only one element changes, but it's a compromise and indexing is made slower to allow for the latter. — They also take up more memory on their own but are allowed to share memory with their copies.
I will say that my ideal language contains them in the standard library alongside standard vectors that index in constant time.
Further, it should be noted that much of the performance talk is on the assumption that accessing from memory is truly random access; — with the existence of c.p.u. caches that assumption is not entirely accurate and accessing from contiguous rather than scattered memory in practice is considerably cheaper so one also pays the price for their being scattered more in memory.
Rich implement his own brand of persistent data structures which makes Clojure's immutability a lot more efficient.
Things like `list.append` modifying in-place might feel like a flaw to some, but I think Python is really consistent when it comes to its behaviour. If you ask a person who comes from an object-oriented world, they'll say it only makes sense for a method on an object to modify that object's data directly.
There's always ways to do things the other way, for example you can use x = [*x, item] to append and create a new copy, while being quite a bit more explicit that a new list is being created.
In pretty much all other languages that have them, the expected behavior of A+=B is exactly the same as A=A+B, except that A is only evaluated once. Now lets look at lists in Python:
xs = [1, 2]
ys = xs
ys = ys + [3]
print(xs, ys)
This prints [1, 2] [1, 2, 3], because the third line created a new list, and made ys reference that. On the other hand, this: xs = [1, 2]
ys = xs
ys += [3]
print(xs, ys)
prints [1, 2, 3] [1, 2, 3], because += changes the list itself, and both xs and ys refer to that same list.(Note that this is not the same as C++, because in the latter, the variables store values directly, while in Python, all variables are references to values.)
The worst part of it is that Python isn't even self-consistent here. If you only define __add__ in your custom class, you can use both + and += with its instances, with the latter behaving normally. But if you define __iadd__, as list does, then you can do whatever you want - and the idiomatic behavior is to modify the instance!
For comparison, C# lets you overload + but not +=, and automatically synthesizes the latter from the former to enforce the correct behavior.
True, though you end up with things like:
' '.join(thelist)
Instead of thelist.join(' ')
Because of the somewhat aggressive mantra to be consistent.The obvious question is why it can't return a reference to the list instead of returning None. I feel like if I've been using the language on an almost daily basis for ten years now and I still get burned by that all the time, then it's just a poorly designed feature.
array = random.shuffle(array)
because I expected it to return a copy or reference, instead making my array None.It would also enable chaining operations:
array = array.append(A).append(B).sort()
In-place vs immutable copy is a language design choice with tradeoffs on both sides, but there's no reason that I can see to not return a reference to the list.Perhaps recognizing this is really the job of an external linter. Sometimes I wonder if the future of enforcing canonical formatting on save like "gofmt" or "black" will extend to auto-correcting certain goofy errors on each save.
mypy would yell at you about this, but afaik type-checked python still isn't the norm.
I think it's pretty off-base to call this a "flaw". Immutable structures have their place and can be very helpful where appropriate, but making its core primitives work this way is far outside the scope or the philosophy of Python. If you want otherwise, you're really wanting an entirely different language. And there's nothing wrong with that! But I think it would be a "flaw" for Python to make these operations immutable, even though I love immutability personally.
I don't know any books for projects per-se, maybe HN will know!
The solutions presented typically include both a "basic" approach, a "as pythonic as possible" approach, and a brief discussion of the trade-offs between elegance and readability, etc.
That said, I think I had good luck with Writing Idiomatic Python.
When I learned Kotlin, I just read through the docs, and then knew of basically all the different concepts in the language.
For Python, the docs were comparably very bad. For instance, Decorators aren't mentioned even once in the "The Python Tutorial". In "The Python Language Reference" (if one even bother to read such a dry document) it's barely mentioned in passing. How should a new user know it's a concept and how to apply it? And the language reference links only to a glossary item, and none of them specify how parameters in a decorator is supposed to work.
Pretty frustrating experience, put me a bit off the language from the get-go.
I once tried to catalogue all the stdlib operations which release the GIL, meaning if you use only those (well, only those "heavy" bits, you can still use other small blocking glue bits), you can do "real" multithreading.
It was a fun exercise!
Edit: Found it! You're in for about 9 hours of quality watching. https://youtube.com/playlist?list=PLwyG5wA5gIzgTFj5KgJJ15lxq...
Me too. When I used to write Ruby I read a lot of CRuby source code. I achieved a much deeper understanding of the language that way. Even answered some really fun stackoverflow questions.
Now the first thing I do when I see a new language is read its source code.
We still have -X dev and sys.flags but it's runtime only.
I still wonder what was the reasoning for allowing creation of local objects with the same name as builtins.
Okay it can be nice to redefine pprint as print I suppose.
Still how many sum, list, min, max, dict(!) have been erroneously redefined in beginner tutorials and beginner code.
From my experience sum and list suffer the most.
Sure there are linters that will warn you but there should be a setting for the interpreter (as in -Werror in GCC) to disallow this silliness.
This is better for novices, because otherwise you create a whole bunch of land mines for people who are desperately trying to get something done. If they aren't aware of the built-in then they aren't trying to use it. Insisting that they become aware of something they don't want right then will be frustrating.
It's also better for experts, in that they're generally aware they're overriding a built-in and are doing it on purpose, and if not they'll have an IDE or linter reminding them.
To me, I see tooling as a spectrum from supportive to controlling. Python is very much on the supportive end. It feels controlling when I get interrupted because some programmer who has never met me programmed a tool to insist I do things their way. That would very much include insisting I respect a bunch of names they decided long ago to put in the global namespace.
A good linter will catch these, so in production environments you usually don’t run into issues. I agree that it can be a beginner trap though!
For students for example, I'll have to agree. Maybe having a flag or environment variable that teachers can set up for it would be a nice idea. You should start a thread on the python-ideas mailing list about this, and it might get somewhere :)
False = None = True
Is probably the most ridiculous thing for a language to support. And yet... $ python2 -c 'False = None = True'
File "<string>", line 1
SyntaxError: cannot assign to None
$ python3 -c 'False = None = True'
File "<string>", line 1
SyntaxError: cannot assign to FalseOne subtle point that the post gets wrong:
> So where does that come from? The answer is that Python stores everything inside dictionaries associated with each local scope. Which means that every piece of code has its own defined “local scope” which is accessed using locals() inside that code, that contains the values corresponding to each variable name.
The dictionary returned by `locals()` is not literally a function's local namespace, it's a copy of that namespace. The actual local namespace is an array that is part of the frame object; in this way, references to local variables may happen much more quickly than would be the case if it had to look each variable up in a dictionary every time.
One consequence of this is that you can't mutate the dict returned by `locals()` in order to change the value of a function-local variable.
Another, less-subtle error in the post is this:
> int is another widely-used, fundamental primitive data type. It’s also the lowest common denominator of 2 other data types: , float and complex. complex is a supertype of float, which, in turn, is a supertype of int.
> What this means is that all ints are valid as a float as well as a complex, but not the other way around. Similarly, all floats are also valid as a complex.
Oh, no no no. Python integers are arbitrary-precision integers. Floats are IEEE 754 double-precision binary floating-point values, and as such only support full integer precision up to 2^53. The int type can represent values beyond that range which the float type cannot.
And while it is true that the complex type is just two floats stuck together, I would very much not call it a supertype. It performs distinct operations.
> Accessing an attribute with obj.x calls the __getattr__ method underneath. Similarly setting a new attribute and deleting an attribute calls __setattr__ and __detattr__ respectively.
Attribute lookup in Python is way more complex than this. It's an enormous tar pit, too much so to detail in this comment, but __getattr__ is most often not involved, and the `object` type doesn't even have a __getattr__ method.
Spot on. Python is widely described as a simple language, but the complexity of attribute lookup is one thing that shows that's not true at all.
Many things in Python are easy, such as adding `@property` above a method definition to turn it into a getter. But `@property` is far from simple - the way it actually works is very complex (for example, properties have to be data descriptors, because non-data descriptors cannot override object attributes of the same name).
> Python is widely described as a simple language, but the complexity
> of attribute lookup is one thing that shows that's not true at all.
Python is a simple language to _learn_. My children learned the basics of Python before their seventh birthdays. But Python is not a simple language to _implement_.The really cool thing about this, how descriptors have their __get__ called, is that methods are implemented this way. So when you access instance.method(), it’s a normal lookup for the attribute named “method”, which is (normally) itself a descriptor, so the __get__ magic is called and this binds the method to the instance at the moment it’s needed! Then you can just call it like a normal function. It’s incredibly elegant but extremely obscure. And vital to understand if you want to dive into monkey patching, which is an incredible skill to have!
Yeah, calling float and complex "supertypes" probably wasn't the best idea, but I couldn't think of a better explanation that wouldn't take too long to explain. I'll ponder about that one.
the getattr thing seems like a huge rabbit hole, I'm totally going to look into this. Thank you :)
And I have no objections to the article's description of the bool type.
https://github.com/django/django/blob/01bf679e59850bb7b3e639...
print([
f"{(not x % 3) * 'Fizz'}{(not x % 5) * 'Buzz'}" or x for x in range(1, 20)
]) >>> l = ['a','b']
>>> l[False]
'a'
>>> l[True]
'b'
>>> d = {0: 'a', 1: 'b'}
>>> d[False]
'a'
>>> d[True]
'b'
>>> # TIL!More pythonic maybe, but you can't have more than a single expression in a list comprehension without it becoming completely unintelligible. I also often miss other standard list features. Reduce, flatmap, indexed versions, utils like first of predicate, split, filternonnull etc
Python's creator, Guido van Rossum, doesn't like functional/functional-ish programming a lot. That's well-known.
Guido: "I value readability and usefulness for real code. There are some places where map() and filter() make sense, and for other places Python has list comprehensions. I ended up hating reduce() because it was almost exclusively used (a) to implement sum(), or (b) to write unreadable code. So we added built-in sum() at the same time we demoted reduce() from a built-in to something in functools (which is a dumping ground for stuff I don't really care about :-)."
Amen.
There are plenty of languages where your code ends up looking like an entry in an obfuscation competition without even trying. If you're using Python, and working for me, I expect the code to be readable by anyone.
And, no, I don't give a toss whether the code is three times the length it might have been if it was dangerously, and expensively, obscure.
>>> max(['aaa', 'bb', 'c'], key=lambda item: len(item))
'aaa' l = []
for a in range(10):
for b in range(10):
for c in range (10):
l.append(a + b + c)
is more intelligible than: l = [
a + b + c
for a in range(10)
for b in range(10)
for c in range(10)
]
???Merely having `__add__` (without `__radd__`) is enough to add two MyNumber classes together in your case.
>It mostly exists to support type annotations,
The link for "type annotations" is broken.
Just `__add__` doesn't work for me. I get:
TypeError: unsupported operand type(s) for +: 'int' and 'Number'
Here's the code: class Number:
def __add__(self, x):
return 42 + x
num = Number()
print(num + num)
Edit: Okay. replacing `42 + x` with `x + 42` actually makes it work. But I'll be honest I have no idea what happened there.(Pseudo code, let's call them num1 and num2 for better readability)
num1 + num2
= num1.__add__(num2)
= num2 + 42 (that's why the order is important)
= num2.__add__(42)
= 42 + 42
= 84I'm not sure about that - it's still `<a href="mypy-guide">type annotations</a>` which jumps to https://sadh.life/post/builtins/mypy-guide and then jumps to your homepage.
def outer_function():
x = 11
def inner_function():
nonlocal x
x = 22
print('Inner x:', x)
inner_funcion()
print('Outer x:', x)
I get how the example works, but don’t see the point of the declaration? If I just left out the “nonlocal x” line, wouldn’t the example still work the same?And that's also usually what you want because otherwise a function would start altering variables in the enclosing scope if they happen to exist!
E.g.
foo = 42
def myfunc(bar):
foo = bar + 1
print(foo)
myfunc(6)
print(foo) # would print "7" if the "foo =" above took the nonlocal foo automatically!
So the trade-off is to require "nonlocal" if you ever need a variable from the enclosing scope.What is your definition of a primitive data types? All of these have object as a superclass, so I wouldn't call them primitive data types in python.
Maybe there is just 1 primitive type: type? Or none at all?
I've been using that instead of print debug, it's been great.
class list:
def __eq__(self, other):
return all(x == y for x, y in zip(self, other))
# Can also be written as:
return all(self[i] == other[i] for i in range(len(self)))
run that with `[1,2,3]` and `[1,2,3,4]` and it'll be true because it only checks up to the 3.It's probably simplest to compare `len(self) == len(other)` before.
Similarly, the set comparison will also be true if the first is a subset of the other.
I can appreciate this must have taken considerable effort, It reads really well. Thank you!
should be
> for index, item in enumerate(menu, start=1):
for the example to be correct :)
https://github.com/satwikkansal/wtfpython
HN thread: https://news.ycombinator.com/item?id=26097732 (163 comments)
PS: found with a site I'm building: https://discussions.xojoc.pw/?q=Understanding+Python+through...