Yes, David Hilbert said if he were awoken in 100 years, his first question would be -has the Riemann Hypothesis been proved- what version of python is in beta?
Regarding the `else` statement. Raymond Hettinger once mentioned he had proposed renaming it to `nobreak`, but in hadn't been accepted. In any case, I consider it the best Python feature with the worst name.
It's not the worst name, it was inspired by assembly loops, where you have an if(generally a jump too but whatever) block which executes iteratively using jumps and we can kind of use an else here
I agree very much with this sentiment. I've used it in many scenarios where it made sense to use it. The feature is great, but the naming could be better. Cool that "else" makes sense in the context of assembly jumps, but it just doesn't make any sense in the context of Python.
To avoid confusion, you have to think that the "break/else" are working together, which means if there is no "break" statement in the loop then there shouldn't have "else"
counter point: funny enough when i taught my ex programming she intuitively guessed the else feature for for loops. i couldnt understand why her code run correctly and ended up finding it in the docs most of us that already program find it unintuitive because no other language does it. but i here its us who cannot think outside of the box and the feature imho actually in wonderful because you actually have to do this quite often and i always hated flag invalidating loops when i had to write them in any language
very interesting. python is not my first language, so usually when i find myself needing a flag in a loop it's a hint that i should write my loop differently. I was taught that you should try to avoid breaking out of loops if you can because it's a little like goto, and we don't use goto because it leads to spaghetti code. although i must confess there are some nice uses for goto in error handling, and I've seen it used effectively in kernel code
Else block has imo fairly solid intuition: You often loop things to find something. Once you find it, you'd break out of the loop, and be happy. However, sometimes you don't find what you were looking for, so you now have to do something... else. With exceptions likewise, the intuition seems clear enough, you expect an exception of some sort... But if you don't get that exception? You do something else. I find it a bit underused syntax tho, and as such, maybe it should be removed. But it's very helpful syntax for many common use cases.
Personally In day to day code; If I have a try...except block I'm NOT expecting a exception to happen, I PREPARE my code to RESPOND to something in the case it happens, but sure enough I'm not expecting it to happen, because the proper and complete execution of the code depends on the program not triggering an exception (I guess there are special task that the normal execution path expects or requires an exception to be triggered... I would never write code like that tho); but depending on the exception to execute to achieve a task seems counterintuitive on 99.99% of tasks. It's like paying insurance: You don't pay because you expect to crash your car, you pay just to be able to handle the UNEXPECTED event of a crash... Just thinking on having to debug code, that depends on a exception to happen to achieve the normal program flow gives me a headache. They are called "exceptions" for a reason: They respond to EXCEPTIONAL conditions in the program, not to the normal and desired conditions. I LIKE the else in the try block, as it allows to handle the correct execution path in a very elegant way, but NOT to respond if there where not an exception that I was expecting for. That's extremely counterintuitive.
It's an unusual if-else +function inline kinda semantic. It is not obvious without context that "else" treats the entire loop as a "condition" with break as true / success. Could just as well have been an if no loops were run "else". Still not that bad to work with
The real problem with “try … except Exception” is that python does not document what exceptions a function can raise, which encourages the use of Exception… 😢
Yes. The exception may occurre at a really deep level. In the Rust language, when a function is able to error, it returns an enum Result with Ok(value) or Err(Error). Yes, enums have values inside in rust.
@@denizsincar29 That's because what Rust calls an "enum", languages with saner naming conventions would call a "sum type". Calling them enums is _really weird._ And yeah on the main topic, catching Exception is _good practice._ What's the alternative, just allow your program to blow up when it comes across an exception type you didn't anticipate? Exception is a base class of the other exceptions for a very good reason.
@@gJonii And then you'll never find out because it won't be logged, your customers will call asking why the service is down, and you'll have no idea why. "Allow it to blow up the program" is never acceptable.
@@isodoubIet This is not necessarily true. Unless the exception is expected and can be handled in some way (maybe how it should be handled is logged and forcing the user to redo the previous step), allowing a program to continue in an invalid state that caused the exception in the first place can lead to problems like security leaks, bugs, etc. In many cases, it is better for a service to be down and fixed rather than broken and running.
Sometime you’d use a for loop to go through some data looking for a feature, then if you don’t find it you’d exhaust the loop and drop to the else block. In that case it’s not success, it’s failure.
The for-else thing caught me off guard. I never used it but I assumed it got triggered only when the body wasn't run, since in most languages the for loop is a while loop with batteries included, and the while loop is an if with a hidden goto. Very, very unexpected behavior!
I mean, isn't that basically what it's doing? When the condition of the loop evaluates to false, the body doesn't run and the else block happens instead. And the only time the condition evaluates to false is when the loop completes naturally
while...else actually makes a lot of sense, seeing that else as the complementary branch to the guard check. Seeing a for loop as a particular instance of while, then, makes it have more sense in the for loop case too
The real issue with `import *` is not shadowing in the way you showed, because you can understand that kind of shadowing statically from your environment. The real issue is actually that you may be deploying your code in an environment where each module has different versions, and if they are using semantic versioning then *adding a new feature* to those modules only bumps the *minor* version, which is assumed to always be backwards compatible. If anything changes that is not backwards compatible, the module would have bumped the *major* version instead, and package managers on the deployment end will use this standard to automatically get the most up-to-date but still compatible version of the dependent modules. However, if you use `import *` then this new feature will be imported into your program, possibly shadowing part of another module that you could not have possibly known about at the time you wrote the code, which turns industry standard backwards compatible updates into automatic code breakage!
In case someone still doesn't know, R language does not have the import as syntax at all, so functions from various libraries often override each other. You often need to use syntax like base::mean(), which means using the mean function from the base library. The dummies behind tidyverse have created some tools, like forcing users to explicitly specify the library origin for each function when namespace conflicts are detected. It's just replacing one nightmare with another nightmare.
One reason for that probably comes from using S3 objects. Because everything is an object, typically all classes implement a global function like `print`, `summary` etc that dispatches when called on that instance. So it’s almost a feature, not a bug to have a global function. But, for some reason we have another OOP system called S4, and another one called reference classes… and another one called R6. The big problem is a lot of it is not standardized, and probably the biggest thing is it’s trying to force too much structure that it becomes too awkward. For example, you can filter from base R and filter from tidyverse, which both do different things but dispatch on a `data.frame` object. Tidyverse technically creates `tibble` objects, which makes a nice separation, but doesn’t force you to only use `tibble`s, and you can `dplyr::filter` on `data.frame` objects. So yeah, it’s complicated haha. I love R and I find it’s very flexible and expressive, but the lack of standardization can be hard to navigate for beginners which is exacerbated by the fact that most non programmers use R. Btw the `box` library is the solution to the `library()` awkwardness you mentioned, it’s heavily inspired by pythons import syntax and creates a module abstraction to compliment the package abstraction python has, in R.
@@samw1248 Thank you for your detailed reply! I completely agree with your points. The lack of standardization in R, especially with the multiple OOP systems like S3, S4, and R6, can be quite challenging, particularly for beginners. Your explanation about the global functions and the complications arising from using different libraries like base R and tidyverse really resonated with me. The `box` library sounds like an excellent solution to the `library()` awkwardness. I'll definitely look into it. Thanks again for sharing your insights!
Unless they've changed this behavior, you _can't_ have it without an "except" even though you can have a "try" without an "except" ("try . . . finally"). Thus, it's really "except . . . else", because either "except" or else "else".
@@Fanta666 This. The alternative is `except` being replaced by `if except`, though the suggested alternate syntax of "noexcept"/"nobreak" is also an agreeable compromise.
@@MAlanThomasII What would you use "try...else" for? If this were valid syntax, I would just remove it because there's no difference between "try: print(1) else: print(2)" and "print(1); print(2)". Don't be silly.
Copying like that is an issue in a lot of languages. In Rust for example, Vec would deep-copy, while Rc would be a shallow copy. And you don't always know from the outside what the fields of a type are, and therefore what x.clone() will do. Same in C++ with vector vs. shared_ptr.
I don’t think it’s fair to say Python’s string literal juxtaposition causes concatenation is “poorly thought out”, because this was a feature of C. In C, it made more sense in the context of macros and automatically generated code. And Python has borrowed a lot of other syntax from C, so at the time *not* having this feature would’ve been more conspicuous.
C doesn't have a string concatenation operator, Python does. Python breaks with tons of C traditions (it's one of very few who put bit operators &, | above comparisons ==, > etc in the priority table!) - and it has a philosophy of 'one correct way', so making the + concatenation optional goes against its core values.
@@sharpfangThe "one correct way" has been broken many times; it is merely a preference. It is in no way to be followed. PEP 584 directly addresses this. "In practice, this preference for “only one way” is frequently violated in Python....We should not be too strict about rejecting useful functionality because it violates “only one way”." The language "violates" the philosophy when there's a good functionality, multiple times throughout its history.
I do agree, implicit string concat is just unnecessary, and it kindof forces one to use a linter to catch stray cases like strings inside a list, for the expense of code looking a bit nicer in some cases.
This topic is very close to my heart. I love Python as a programming language but I have faced these issues. Since I code in multiple languages, I have been gravitating more towards syntactically rigid languages.
Way back when python first started catching on, there were some variants which added back in (optional) typing, blocks denoted by curly braces, ect. I liked that. But alas, most folks didn't... The lack of strictness is a bit of a tradeoff between ease for small stuff and scripts, and making it harder for large/complicated things. However, the real brilliance of python IMO is being able to fairly easily include lower level C and C++ code as modules. It also beats the hell out of perl
One thing close to copies is when you try to initialize a 2d array like this: a = [[0]*5]*5, it wouldn't do a proper 2d array (an array with multiple different arrays in it), but an array with multiple references of the same array, so if you were to go a[0][0] = 1, it would change the first elements in all of the rows, not only the first one
Oh dear, I did not know that... although I think the only time I ever used that, was when I was creating a numpy array, which I'm pretty sure creates a deepcopy.
11:55 if the editor or IDE was to feature disambiguation of imports, could that present a code execution vulnerability given how flexible python module definitions are? At that point, developers would need to read the source of modules for verifying the lack of possiblly malicious code execution (on top of the normal benefits and tradeoffs)
I think shallow copies make more sense than you'd realize. Generally speaking you want to copy the least ammount of memory possible and be very explicit over deep copies.
@@craftylord3336 It kinda makes sense when you realize that all these languages work with reference types and primitive types. A reference-type is just a pointer to the real data, so when you copy the shallow copy is justified by this same reason (the pointer is copied). You generally don't want deep copies when you have this kind of reference-type structure as it would blow your memory and also your GC, and because most of the time what do you need is a shallow copy.
@@craftylord3336 You use = to set these types that are what I so called primitive types. And even if they were not primitives it would still make sense because their memory layout is entirely flat. When you write the number 100 the number is fixed and cannot be changed, every single bit required for it's identity (which is value-based) is already here, so you use = to set it to another number (the same for the other ones). Also, about the function called copy vs function called "pointer" I don't get it, no single language has a function called "pointer" because it does not make sense, a pointer is just a reference to an object somewhere in memory, underlying it is a number (like an 'int') so when you call 'copy' you are really copying everything that is flat there (including this int), just not the pointed object itself which is on another place in memory. If you want a language that does deep copies of your lists use some that don't have reference types like C, C++ or Rust, most modern languages that have references (including C#, Kotlin, Java, Dart, JS and Python) suffer from the same thing you called a "poor design" that in reality just makes sense if you think about it for a minute.
@@craftylord3336= does copy the reference, not the object. Test it with the `is` operator! I disagree with the shallow copy vs deep copy part. That would be really unexpected coming from any other GCed language. How does it even handle ref-loops? Haven't tried Python's deep copy, am on a phone, but does it crash, fill up memory, somehow track all objects and try to recreate the loops? How does deep copy handle custom objects? There's no one way to copy objects in Python, there is no copy interface/protocol last time I checked. Seems like a deep copy function would be full of hacks.
@@craftylord3336 Python uses variable length integers. For small values I assume it uses some tagged pointer optimizations, but speaking in the general case int values are (immutable) objects allocated on the Python heap. Tagged pointer optimizations are implementation details and semantically it still behaves the same.
6:34 i feel like there are some scenarios where "Exception" makes sense, tho. i often find myself running into scenarios where 2-3 different exceptions can occur in a try/catch block, but i intend for the program to handle any of those exception types the same way (a good example could be a method that expects an integer as a string, and should do the same thing in the event that the input is either null or a letter). why waste space with redundant code, or risk forgetting a specific exception type that could occur there, when you could just catch all of them and handle them all accordingly ? in that specific case, I don't see that as being lazy or careless, but rather as being thorough to make sure the code doesn't crash for users
I think what the video creator meant was that an exception shouldn't be broader than it needs to be. Obviously, if you _do_ wish to catch all exceptions, then Exception is the way to go.
An addition to the star imports: Not using star imports also benefits to the speed and the file size of your application. If you use a big library like PySide6 (for creating GUIs) and you import everything, your compiled app will be roundabout 200-300 megabytes. If you only use the Widgets, Gui and Core (which most applications do), then you will end up with like 20 megabytes and a MUCH better startup time AND in addition to that it also helps your IDE, as it doesn't have to index dozens of docstrings and functions. But if you only use small libraries like colorama it doesn't really matter, but still a good habbit to not do star imports :)
@@jogadorjnc a += b is equivalent to a.extend(b), which mutates a in place rather than creating a new list a = a + b creates new list and assigns it (the behavior that both should do) 🤮
@@KirkWaiblinger Wait, why would you ever want the 2nd one? Isn't it always slower and more memory hungry? Also, thinking about it now, it kind of makes sense, the += operator is doing the same thing, but taking advantage of its use being more restrictive to be more efficient Edit: Maybe I'm just too OOP-pilled at this point, I just see objects and methods with fancy shortcuts to use them
@@jogadorjnc you might well want the extend behavior... in which case, you should write a.extend(b) ("implicit is better than explicit" and all that). Having a += b not equivalent to a = a + b is nuts. The trouble is, you can go a long time not realizing that they have different behavior until you get some subtle bug due to having mutated a reference you passed somewhere. And a += b is pretty far down on your list of things that might be suspicious when debugging the problem
Implicit string concatenation is great, it allows to do multiline strings but without losing the indentation, it's a feature I'd wish to see in some other languages
The last feature is quite expected. After all, a list in the memory of a computer is just a pointer to a memory address :) Actually I mostly write code in php and its copy-on-write behavior was confusing me for a long time in the past.
Regarding splitting a long string over mutliple lines, i was gonna say i would write this in a triple quoted string and then unfold it with some stdlib tools. But when i looked for such tools i realized that neither textwrap nor shlex have them ready to go. You would have to do it in two steps (like textwrap.dedent().replace(" ", "") ) or use a full on regular expression. Or i guess you could make a proper tuple of your string lines and use str.join(). There's a lot of option, but they all seem like a lot (ish) of work just to make your source code prettier :)
Great video, thank you. I am mainly a C# and JS dev but like to see what other languages share/do differently. What about else with 0 runs? So while (false) else print('yes') or no? Why would you use "else" in a real example? Just to avoid checking count/state afterwards like if count < length / isAllGood = false?
wow using else in a for loop as a sort of confirmation that everything had looped through successfully was something I would have never have thought of. though it does make a lot of sense in a while loop. I literally used it in my very first python script to close off a loop. It just seemed very intuitive to me since while is a sort of logical statement. It also makes sense in a try block. Less obvious but still it makes total sense. Actually all of them do make kind of sense if you just build a regular sentence out of them like: - if condition is true do a thing else do another thing - while condition is true do a thing else do another thing - try to do a thing, if it breaks do another thing, else do a different thing - for current thing in collection of things do a thing, else do another thing for is definitely the most confusing one though. Especially with break although it does make sense if you understand what break breaks you out of the current block entirely and else is part of that block so it won't be run if interrupted by a break.
My favourite mis-feature is the syntax for single-element tuples. When moving around code I often wind up with something like channel_id = "indently", and then trip up later as channel_id is now a tuple, not a str.
I personally use for else in my code, but i you are also right, it doesn't justify for what it actually means. I used to use from module import * but then i got to know the importance and i don't use it. And btw i never knew the difference between shallow copy and deepcofee until i watched this video 😅
I know when you're using default mutables for a dataclass it requires you to use a function that returns the mutable to get around this, would that work in an ordinary function call as well? I don't think it's any easier to read than the boilerplate you have, but it would be a different way of doing it
import * and mutable defaults are both caught by pylint, at least. But yeah these aspects of Python all have sharp corners. Also, when did the | syntax for type hints show up? I use typing.Optional and typing.Union since I wasn't aware of that bit of syntax sugar.
I thought that you goinng to say, that else is worst feachure bc you can mistakenly make else not for if, but for for, like: for i in range(10): if i == 5: print(five) -else:- -print(i)- _else:_ _print(i)_ and you get an error
@@SonOfMeme It's always baffled me how people will complain about the whitespace in Python, but then if you don't use whitespace "properly" in their language of choice they bitch about it. Vestigial semicolons and meaningless whitespace... why?
I also don't like how enums work, the fact that you have to call auto() for each of them when in 90% of applications you are going to use auto anyway. Why not make that the default, while still allowing overwriting with a value in the rare cases you need a specific value. Also having to import packages for such basic features always seemed a bit hacky. Almost like its not part of python but you had to rely on someone else's implementation.
I am starting to learn Python and even if the subjects are more relevant to people who already have mastery of it, I found it very interesting to follow the video (by reproducing the examples, because I learn better by doing it even if it's shown, that way I can test a little more) And I already liked seeing certain practices often seen in tutorials which could go against the good practices that you mentioned and therefore avoid getting into bad habits and in addition I learned some things with the video that I I don't know enough about it yet but it will probably be useful to me one day.
You see, try, except, else works for me. I would agree with you however that in the case of 'for' and 'while', it does seem unintuitive... but, hey, at least I learnt something more about looping! 😊
the 'else' in 'for' and 'while' I would expect means 'if there were no elements reached by the loop' which is nearly the opposite of what it actually means
@@MagicGonadsThe idea is that you'd often loop to find some particular element. If you find it, you break out of the loop and continue from there. But if you reach the end of the iterator... Well, now, you need to do something else. This something else in case of this failure, you'd put in the else block, knowing it's only ran if you failed to break out of the loop.
@@gJonii but semantically 'for all of these things, otherwise this' is what a construct 'for-else' would mean intuitively, is what I'm saying. For loops may often be a search, but not every for loop is a search.
Yeah, its very confusing in the for and while loops, and even for the try/except i feel like its not even worth it. A "nobreak" or even a good old "then" would make it much clearer But the whole thing could be much less ambiguous by explicitly setting a boolean variable (e.g. found, error, etc) before the loop and changing that variable in the same line as the break/exception, then using an if after the loop to explicitly run some code if the variable was changed. You don't need a new keyword for every possible scenario, or else we'll end up with a "noop" keyword for when the loop is iterating over an empty list or something
@@sutirk yeah my interpretation of how 'else' would work would also be called 'empty' (the case in which the iterator is empty) and often you just handle this explicitly
the else block is like the exact opposite of what you would think it doesn't even make sense compared to how it works with if if anything it should run only when broken out i think it shouldve been named 'also' block
The use case presented for it is element search. You loop over an iterator, searching for some element. If you find it, you'd have "if element == target: do stuff; break" But now you'd write code after the loop. Can you trust you've found the element? Perhaps not. Perhaps your loop just ended naturally, and your cool break logic never ran. What to do then? How would you even know that happened? Enter else-block. It's only ran in this scenario, so you know your break-logic was never ran. You'd have absolutely no benefit from this also-block that runs if broken out from loop, since you could put this logic manually to the "if condition: break" section for much more readability.
FYI if you want to run code when a for loop is broken, the way you would do that is to put the code before the break. Something like: for x in xs: if x is None: print("Got unexpected value, breaking loop") break else: print("Processed all values successfully") You can also kind of see how it DOES make sense with the if. In this example, which is how for...else is usually used, the "else" only runs if the "if" never runs. In expanded form, the above code translates to something like this: if xs[0] is None: ... elif xs[1] is None: ... elif xs[2] is None: ... else: print("Processed all values successfully")
@@gJoniithat makes sense but it’s weird to me that python cares about this very niche use case but doesn’t have named breaks to allow breaking out of multiple nested loops. Rust lets you break out to any scope you want by name and even “return” a value with your break statement which can be used to solve this problem too. I mean I get it, python is much older and is full of tons of design decisions that we wouldn’t choose again knowing what we know now. But it’s just a bit frustrating when a “low level” language lets me often write higher level code than a “high level” language.
You'd almost always want to use a shallow copy on a list containing immutable data. Like a list of strings: A = ["1", "2", "3"] B = a.copy() B[1] = "c" print(A) # ["1", "2", "3"] print(B) # ["1", "c", "3"] Strings are immutable in Python, so you never have to worry about the pitfalls of modifications to B propagating to A. This means that A and B require less memory to store than if B deep copied A, because they both have the same references to elements 0 and 2. So only 2 new objects have to be created (B and "c"). A deep copy would require 5 new objects be created (B, "1", "2", "3", and "c").
About the “shallow copy” issue. This sort of thing is a major caveat of working with super high level, high abstraction languages. Not only you’re not expected to manage memory, it is actually hidden from you. The notion that a list is accessed by either a pointer to a contiguous block of memory or a pointer to the head of a linked list is missed by many. When you write “a = [ 1, 2]”, the variable “a” is not the list itself, it is not automatically aware of the data 1 and 2. “a” is just a reference to the list. This is what you’re copying around by default.
does the shallow copy method not do the exact same thing as just setting a_copy = a? Since in both cases the variables both point to the exact same data if I'm understanding correctly. So then why is there an explicit copy method when it does the same as just assignment, you would expect it to do more than that, such as actually deep copy
What I'm understanding is that list.copy() DOESNT fully copy the nested list. It only fully copies the outer layer elements, and creates a reference to the inner list (which comes from the same memory location as the original list) and thus, editing the nested list of the copy created with the list.copy() method would actually be altering the original nested list that is referenced in the copies list.
@@valerielboss yes, so a_copy[0] *= 2 will also change a[0]. One way to test is evaluate the boolean: >>>a is a_copy False which compares the id()'s. So in his 1st example: >>>a_copy == a, a_copy[1] is a[1] (True, True) while: >>>a_copy is a False. (but I didn't test those in an interpreter, but all pyhtonistas should try it out to learn it).
It gets easier to understand the behavior if you understand memory and pointers/references. In his example, you have a = [1, ["a", "b"], 2] What that actually means is (in some very generic pseudocode notation): mem0 = ["a", "b"] pointer0 = address(mem0) mem1 = [1, pointer0, 2] a = address(mem1) So what "a_copy = a" does is that both variables point to the same memory address: a_copy = address(mem1) In this case both a and a_copy are exactly the same, they point to the same memory and have the same content. The shallow copy "a_copy = a.copy()" copies that region of memory along with every *value* in it: mem2 = [1, pointer0, 2] a_copy = address(mem2) In this case, a and a_copy have exactly the same content, but one of its values is a pointer to another object, so both a and a_copy share this object, and when this object changes it will affect both a and a_copy. Finally, the deep copy "a_copy = deepcopy(a)" actually goes into every pointer and copies that memory region too, saving the new memory addresses in the list: mem3 = ["a", "b"] pointer3 = address(mem3) mem4 = [1, pointer3, 2] a_copy = address(mem4) In this case, a and a_copy have the exact same values, but are completely independent, whatever you change in one will never affect the other, because a references pointer0, whilst a_copy references pointer3.
instead of memorizing deepcopy to be imported from copy, can't I just use new_list = [element for element in old_list if element in old_list] ? ie a list comprehension. I think that is easier overall than memorizing a whole class and method narrowly defined as deepcopy.
So you'd rather cleanup fails in your code because you didn't realize that some library that used another library that used another library didn't document one of the errors that can be raised deeper in the call stack, right? At least catch an Exception and log if the type of exception is unknown, unless you're absolutely sure the function raises only what you know it raises. Or unless your code doesn't have any important cleanup to do.
If I make a shallow copy, is there any way to display the list that displays the references so that I _know_ I'm dealing with a shallow copy? (I figure this might be useful in debugging.)
The else in try block makes sense to me as I've always understood it as "(if) except: ... else (no exception: ... The else in the loops is less intuitive to me. It seemed to me like it should run if there were no iterations at all
Yeah I agree, the for-else blocks require you to think of a for loop as a series of checks for which "breaking" is the sign you've found what you're looking for. Most people are taught that loops are for doing something, and break is for when you want to stop doing that thing early...which is kind of conceptually the opposite. I don't read it as applying if there were no interactions at all though, I read it as "if any of them failed (had a break)"
it does run the else block if the loop had no iterations. It's useful if say you count successes: for count, item in enumerate(container, start=1): break else: count = 0 print(f"Found {count} items) w/o the else block, you get a NameError, and to prevent that you would need to predefine count=0, which is U G L Y, and unpythonic.
Regarding the mutable defaults, when you fixed it why did you write “target | None = None”? Inside the function you checked to see if target was None anyway, so why not just make it “target = None”?
The fifth example caught me out causing hours of trying to figure out what I’d done wrong! I had no idea there was such a thing as a shallow copy. It was only after asking for help on a forum that I understood what was happening.
From a Python beginner: • Are there any benefits of using deepcopy vs a_copy = a[:]? • There’s no need to import when using a[:]. • Could this syntax be a fairly new addition?
@@IndentlyAs said, I’m a beginner, but would there be any benefit in the supplied example (17:41)? The contained list - [‘a’, ‘b’] - is hard coded. I understand that there would have been a difference if the list in the variable “a” were to have contained another list variable. Example: a = [1, 2] b = [a, 3] b_copy = b[:] Then b_copy would, in my understanding, be affected by changes in a, but not by changes in b, nor b be affected by changes in b_copy. By the way, thanks for your informative videos.
That's correct. The slice operator creates a new object with shallow copies of the objects in the sublist you picked. Shallow copies of basic data types are just a new copy of the data. Shallow copies of complex data types are not (Technically it's a new copy of the pointer pointing to the object, but then I'd have to talk about pointers).
@@bjorn_ It depends on what the type is of the value you're operating on in any given list. When you make a shallow copy of a list, you create a new list containing references to the same elements held by the original list. This means that if the original list contains primitive types (like integers or strings), they appear to be copied. But in reality, the new list simply points to the same memory locations. If the original list contains mutable objects (like lists or dictionaries), these are not copied; both the original and copied list refer to the same objects. So, if you modify a mutable object in one list, the change is reflected in the other. On the other hand, when you make a deep copy of a list, you create a new list and also create new copies of every item contained in the original list. This includes creating copies of all mutable objects. So, if you modify an object in one list, it does not affect the other list. Here's an example: from copy import deepcopy # Original list a = [1, 2] b = [a, 3] # Shallow copy b_copy = b[:] b_copy[0][0] = 'x' print(a) # Output: ['x', 2] # Deep copy a = [1, 2] b = [a, 3] b_deep_copy = deepcopy(b) b_deep_copy[0][0] = 'x' print(a) # Output: [1, 2]
15:35 can someone explain to me(very green and curious programmer) why not to declare this list like a = [1, [a, b], 2]? Tbh didn`t even know that you can declare variables in python like you did
These are type hints, they are relatively new to the language and optional. I'm pretty sure that they are not enforced at runtime, but the editor can use them to point out mistakes that you might have made.
5:31 If I made the language, I would just implement this differently. With an if statement, else only runs when the if statement did not run, so I would make for and while do the same thing: the else only runs if the loop is never initially triggered. Essentially: while(Initially true statement): while code runs else: else code does not run, no matter how few times the while code ran while(Initially false statement): while code does not run, not even once else: else code does run for i in (Some data): for code runs else: else only runs if there was no data to start the for loop I think this is more useful because not being able to start a for loop or while usually means 1 of these 3 things happened: 1. we hit a base case in some kind of recursive code, which might need code that only runs in the base case 2. we had no data to process, so we might need to load in default data, or respond to the empty data in some particular way
Both of those statements are false, and you can verify it by using what you wrote in a Python script. I'd encourage anyone who posts information to check it before sharing it on the internet.
regarding star imports: pylint will throw various warnings at you about them, such as to not use them in general, but it also has a lint for star imports shadowing each other. pylint, with a lot of configuration about which issues you care about, is actually a very nice tool to use in any project. there really isn't an alternative for it yet.
I always import the entire module/package instead of importing single functions, is this bad practice? I prefer to access copy.deepcopy() than to access it as deepcopy() because if someone is reading or glancing at the code they will think deepcopy is an independent module/package
8:15: I always thought this construct was called "finally", particularly in a Try block. 11:30 I never liked star importing even though it was constantly referenced in Python documentation. Even as a novice programmer (hey, I'm still a novice) I always thought there was a danger of conflicts and confusion by bringing everything into the same namespace. 17:45 This is the best demonstration of deep versus shallow copying I've ever seen. Now I understand it. And this is definitely completely unintuitive behaviour. Today I was trying to do some scripting in Excelscript; the darn thing is a nightmare, mostly due to some incredibly questionable choices in the API that makes everything far more complicated than it needs to be. Your Python complaints are comparatively mild compared to my complaints about Excelscript.
Thanks for the useful video; I learned some things. For implicit string concatenation, that comes in very handy when needing to do deeper escaping of strings containing a mix of single and double quotes. Used right, it's a huge help. I agree that used wrong, it's a mess. Maybe best to not use it unless it's actually needed. Great video overall, though. Thanks!
I agree with you when it comes to "else" blocks on loops, but try/except/else actually makes perfect sense to me (first try this, then if there is an exception do this, otherwise do this), and it can be very useful, especially in cases where you want to do something with a result that won't be there in the exceptional case, without accidentally catching errors you didn't mean to. I'd even go so far as to say that if you find yourself writing more than a single function call inside a "try" block, you should consider whether some of it should go in an associated "else" block instead.
Good list learnt something new today. 1.) Didn't know this, can't really see a use for it and can see how that would be annoying. 2.) Didn't know this either, could be useful. 3.) Did know this, but never use star imports personally. 4,) Bit by this before, when my editor didn't warn me. I spent hours trying to figure out why something wasn't working. 5.) Come up against this before but don't think it's too bad.
I'm totally with you with the first 4 features, but the last one do you have in any language i know, because of the reference type of the nested list (or to be clearer in python because of the mutable type, because in the end everthing is a reference type in python). Therefore, copy behave as expected in my opinion. What would be nice on the other hand, an additional deepcopy method for example.
@@isodoubIetC++ is a lower level language where you are usually preoccupied with memory management and performance. In higher level and usually interpreted languages it's much more common to see pass-by-reference as the default, at least for object types. That would include JavaScript and consorts, PHP, Ruby, C#, Java,... Problem is, it always comes with an overhead, usually either reference counting, garbage collection or both, because you have to keep track of where the object is still needed or not. That's not an acceptable tradeoff for a systems level language like C++ or Rust, but you can always implement your own if you so desire.
@@eldonad It has nothing to do with C++'s focus on performance. It's just a conscious design choice based on the idea that it's much easier to reason about programs where your objects behave just as the built-in types.
@@isodoubIet Ok, I've thought about it for a bit, and I can imagine a weird version of C++ where objects are passed by reference by default, so I stand corrected. However I still think passing by value as a default is more natural in runtimes with unmanaged memory, since in that case specifying the flavour of reference you use can provide you with information you wouldn't care about in a garbage collected runtime. But eh, at the end of the day every language is kind of pass by value at heart, only that the value can be a magic handle to an object, or a shared_ptr...
I think the string concatenation wasn't explicitly thought out, but brought from c, as python is compiled to C. In C, you can do the same thing with strings.
In the Ruby programming language, the role of the "else" keyword, as described in the second section, is performed by the "ensure" keyword. I think it's a much better name. It's also slightly different, because the "ensure" code block is always executed.
Honestly the mutable defaults issue is easily the worst thing here. I have never once seen anyone have the issue with missing commas in a string list. I don't understand at all why `try:`, `except:`, `else:` doesn't make sense to you. It comes right after the `except`, so, it's `else, if no exception`.
Absolutely valid criticisms for the string concatenation and shallow copies. There is also a problem when you're trying to make a list with multiple copies of the same thing (ie. lista = [[item1], [item2]] # if you do [[item1] * 2, [item2]] * 2 and then try to adjust item 1, it will adjust all of the first elements of all of the copies of lista. There is a way around it, but to find out the easy solution you have to go to the Q&A section of the documentation -_- I disagree on you star imports point, if you are making a function that is already defined... I feel like you're setting yourself up for failure! Why would you do that?! But yeah, good video.
I don’t understand why python breaks function scope like this. Any new call to the function should also make the default parameter values different memory locations from previous calls. Or better yet, once the function call ends all the internal variables can be marked for GC and ignored by the interpreter
The deepcopy can yield unexpected behaviors when it acts on objects without recursive memory calls, which may be another reason for why it's not default. I don't know what the cause of these unexpected behaviors are, but I've run into situations where performing a deepcopy on an object makes it unusable, while performing a shallow copy works perfectly. My guess is that more complex integrated objects are more likely to have internal parameters that you don't want to copy, and so are more likely to want to be shallowly copied instead of deepcopied.
I think the worst feature is the copy, maybe is made it with shallow copy because Python itself is heavy. But I think they should change to specific copy like a.shallow_copy() instead of only copy method.
List comprehension usage would be useful for a lot of these vs what you do like in mutable example . Or using inline defaults Time and performance gains too
I mean... For try/except/else, the else is after the exceptions. So it's like, "else, if there are no exceptions, do this". Makes sense to me. For for loops, it makes even more sense. You break out of the loop because you are done with whatever you were doing. If you reach the end of the loop, you're probably not done, so you fall through into the else block. Best example is searching for a particular element. Once you find it, you break out of the loop. If you reach the end, you didn't find it.
It's also strange that shallow copy can be created by calling a method of lists with `list.copy()` whereas deep copy requires to be done with `deepcopy(list)`, which is imported from another module called `copy`😧...
I've been trying to get back to programing Python more intensely after many years; and one of the things that has been bothering me is it has way too many ways to do the same thing to different things, but it's not always done the same way for different types sometimes; and it can get even worse when using libraries that come with their own additional types...
Calling the else branch of the for or while loops a success is somewhat questionable. One could consider the break statement execution to be a success instead, actually. For example, it might mean we found something we looked for.
My least favorite thing in python is the bytes() constructor because it has one notable inconsistency with the str() constructor that is inconsistent with the other constructors in the same space. Here's an example: A = "1" int(A) # => the integer 1 str(int(A)) # Now we've roundtripped back to the string "1" A = "1" bytes(int(A)) # this is b"\x00", ie the null byte. Unlike the str() constructor which turns an integer into a decimal string representation of the number, the bytes() constructor creates a byte string with as many null bytes as the integer specified.
bytes() is explicitly made to work with ASCII text, why would you pass in an int? I assume that passing an int works as a handy way to get x number of NUL bytes because otherwise it would be incredibly ambiguous. In your case, should bytes(int("1")) be parsed as 1 in hex (\x01) or as the string "1" (\x31)? What if we pass in bytes(int("111"))? Do we expect it to give us the character "o" (\x6f) or the character "1" three times (\x31\x31\x31)? I guess you can see how it would be useless either way because you're either limited by only outputting the bytes 1-9 over and over again; or your input would have to be made of a concatenated mess of a bunch of decimal values for characters making a truly meaningless int, and which would be even more ambiguous to parse if you consider multiple characters, and then extended ascii and encodings like UTF-8...
Your first question can be answered with a similar question: str() is explicitly made to work with abstract text, why would you pass in an int? bytes(int("1")) => b"1" bytes(int("111")) => b"111" Rationale: int(b"1") => 1 int(b"111") => 111 For non-ascii:: int(u"一") => ValueError, only characters 0-9 are recognized so bytes doesn't have to handle that either. My view on this in general, which should address your arguments that I did not explicitly mention above: mapping an int to bytes *is* ambiguous but it is the same amount of ambiguity as mapping an int to a str and mapping bytes to ints. The decision as to which of the possible outcomes Python will use for those values has been made. So for roundtripping with int and symmetry with str(), bytes() should have been implemented with the same choice.
I had never any problems to remember using else with the try statement. You always have to get the idea of a language, it doesn't matter if its a spoken language or a programming language. Python reduces the reserved words like "else" by using them in a slightly different context with several statements or using well known statements from other languages in a different way. So "try" is a special use case "if" to handle exceptions instead of logical expressions, the "for" loop is in fact a special case of the while loop with an implicit exception handling. I never thought about it in a negative way, sometimes it took me only some time to really understand the idea of the statement.
"else" with while and for loops works completely different than other uses of the "else" keyword, the behaviour doesn't really have anything in common, that's why it's bad
The thing that makes it worse, is that in if else blocks, else gets ran when the if condition is false, whereas else gets ran when a try or while loop finishes successfully. Now yes, you could make the argument that else gets ran when the while condition returns false, but that's not the way people think about it.
@@b4ttlemast0r The else makes sense if you think about how it is implemented. A while loop would be something like this (Yes, this is a mix of asm and python): :loopstart # your loop content if loop_cond: goto loopstart else: # your "else" code :loopend Now you imagine "break" as "goto loopend". It is similar for try: # try-block if error_happened: # except block else: # else block # finally block I hope you can understand what I mean, this is how I memorize it :)
Well, use a linter 😸 It scolds you for star-import and non-specific exceptions. Maybe it can warn about implicit concatenation (or you can instruct it to). And IDE usually shows some warnings.
Yeah, that's all well and good, but a language should be intuitive without a linter. Having to rely on a linter to remember how to do unintuitive stuff feels like having to walk around with a textbook on English grammar just to go to the grocery store down the street.
@@oliver_twistor The problem with programming languages is that they instruct computer to do what is written, not what was intended. For the grocery store, a "point and click^H say 'this one'" interface is usually enough :)
I often use a loop to search for something, and use "break" to break out of the loop when it is found. Then "else" means it was not found, which kind of makes sense to me.
This is unrelated to your section on .copy(), but wouldn’t it be more straightforward to just make a copy of the mutable defaults in the first line of the function? target_copy = target.copy() target_copy.append(name) return target_copy
he agreed, his only 0xDEADBEEF was the name in the for/while context. I do love the construction, and have no problem with the name...for me it's "BREAK or ELSE"....
Totally agree on the else for the reason; if you don't go into the if you go into the else. So it would make intuitive sense if you don't go into the loop block you go into the else rather than its present logic. And there is far more cases where it would be useful to use else if you cant loop, rather than if you can.
That else block to me (java developer) seems quite unintuitive, it is a success-block complete-block? Why name it else (other than the fact that it already is a keyword I guess)?
I think I've seen "try...else" in other languages too. So I wouldn't have an issue understanding it. But on the other hand: What's the purpose? You don't really need it at all. Might be handy with loops but within a try block you can just put the "success" as the last statement.
Nice video ! Usually i put inside the try: some lines to be executed after de dangerous code, if nothing triggers an exception that code will execute, otherwise it won't... so i don't really get the purpose of the else: at all...
If the line you thought was dangerous doesn't throw, but a line afterwards does, you may be catching different behavior than you expect. So using the else can help you be precise about what you know how to catch and handle vs what should propagate an error
I read this title as Python 5 and thought I woke from a coma
Python is learning from iPhone and just skipping numbers that are bad for marketing, like the unlucky number 4 in Japan xD
I thought it said "Python 5," too, and I was initially like, "Wait a minute, what the hell?"
Wait till you hear about Python 95 and Python 98 😂
Yes, David Hilbert said if he were awoken in 100 years, his first question would be -has the Riemann Hypothesis been proved- what version of python is in beta?
commenting to share that was my immediate reason for clicking this video too lol
"It will print nothing because we didn't print anything"
-Python development in a nutshell
Regarding the `else` statement. Raymond Hettinger once mentioned he had proposed renaming it to `nobreak`, but in hadn't been accepted. In any case, I consider it the best Python feature with the worst name.
Agreed
I learned something today.
ooo... I like that.
It's not the worst name, it was inspired by assembly loops, where you have an if(generally a jump too but whatever) block which executes iteratively using jumps and we can kind of use an else here
I agree very much with this sentiment. I've used it in many scenarios where it made sense to use it. The feature is great, but the naming could be better. Cool that "else" makes sense in the context of assembly jumps, but it just doesn't make any sense in the context of Python.
To avoid confusion, you have to think that the "break/else" are working together, which means if there is no "break" statement in the loop then there shouldn't have "else"
but they are on the different levels of nesting... it's counterintuitive
And also, isn't else usually run when an if-statement 'fails'. So shouldn't else run when the for loop *fails* (breaks)@@vvhitevvizard_
counter point:
funny enough when i taught my ex programming she intuitively guessed the else feature for for loops. i couldnt understand why her code run correctly and ended up finding it in the docs
most of us that already program find it unintuitive because no other language does it. but i here its us who cannot think outside of the box and the feature imho actually in wonderful because you actually have to do this quite often and i always hated flag invalidating loops when i had to write them in any language
very interesting. python is not my first language, so usually when i find myself needing a flag in a loop it's a hint that i should write my loop differently. I was taught that you should try to avoid breaking out of loops if you can because it's a little like goto, and we don't use goto because it leads to spaghetti code. although i must confess there are some nice uses for goto in error handling, and I've seen it used effectively in kernel code
Else block has imo fairly solid intuition: You often loop things to find something. Once you find it, you'd break out of the loop, and be happy.
However, sometimes you don't find what you were looking for, so you now have to do something... else.
With exceptions likewise, the intuition seems clear enough, you expect an exception of some sort... But if you don't get that exception? You do something else.
I find it a bit underused syntax tho, and as such, maybe it should be removed. But it's very helpful syntax for many common use cases.
Personally In day to day code; If I have a try...except block I'm NOT expecting a exception to happen, I PREPARE my code to RESPOND to something in the case it happens, but sure enough I'm not expecting it to happen, because the proper and complete execution of the code depends on the program not triggering an exception (I guess there are special task that the normal execution path expects or requires an exception to be triggered... I would never write code like that tho); but depending on the exception to execute to achieve a task seems counterintuitive on 99.99% of tasks. It's like paying insurance: You don't pay because you expect to crash your car, you pay just to be able to handle the UNEXPECTED event of a crash... Just thinking on having to debug code, that depends on a exception to happen to achieve the normal program flow gives me a headache. They are called "exceptions" for a reason: They respond to EXCEPTIONAL conditions in the program, not to the normal and desired conditions. I LIKE the else in the try block, as it allows to handle the correct execution path in a very elegant way, but NOT to respond if there where not an exception that I was expecting for. That's extremely counterintuitive.
@@setsunaes For loop iteration for example depends on the iterable sending "loop over" exception. As an example of expected exception
It's an unusual if-else +function inline kinda semantic. It is not obvious without context that "else" treats the entire loop as a "condition" with break as true / success. Could just as well have been an if no loops were run "else". Still not that bad to work with
@@setsunaes in python exceptions are just code control like extra return values lol
The real problem with “try … except Exception” is that python does not document what exceptions a function can raise, which encourages the use of Exception… 😢
Yes. The exception may occurre at a really deep level.
In the Rust language, when a function is able to error, it returns an enum Result with Ok(value) or Err(Error). Yes, enums have values inside in rust.
@@denizsincar29 That's because what Rust calls an "enum", languages with saner naming conventions would call a "sum type". Calling them enums is _really weird._
And yeah on the main topic, catching Exception is _good practice._ What's the alternative, just allow your program to blow up when it comes across an exception type you didn't anticipate? Exception is a base class of the other exceptions for a very good reason.
@@isodoubIet If there's an exception of type you didn't anticipate, it seems the only sane way to handle it is to allow it to blow up the program.
@@gJonii And then you'll never find out because it won't be logged, your customers will call asking why the service is down, and you'll have no idea why. "Allow it to blow up the program" is never acceptable.
@@isodoubIet This is not necessarily true. Unless the exception is expected and can be handled in some way (maybe how it should be handled is logged and forcing the user to redo the previous step), allowing a program to continue in an invalid state that caused the exception in the first place can lead to problems like security leaks, bugs, etc. In many cases, it is better for a service to be down and fixed rather than broken and running.
Sometime you’d use a for loop to go through some data looking for a feature, then if you don’t find it you’d exhaust the loop and drop to the else block. In that case it’s not success, it’s failure.
The for-else thing caught me off guard. I never used it but I assumed it got triggered only when the body wasn't run, since in most languages the for loop is a while loop with batteries included, and the while loop is an if with a hidden goto. Very, very unexpected behavior!
completely agree, that's the one that's totally unintuitive. It should be for ... then ...
@@francoismolinier6924 this makes a lot more sense!
I mean, isn't that basically what it's doing? When the condition of the loop evaluates to false, the body doesn't run and the else block happens instead. And the only time the condition evaluates to false is when the loop completes naturally
while...else actually makes a lot of sense, seeing that else as the complementary branch to the guard check.
Seeing a for loop as a particular instance of while, then, makes it have more sense in the for loop case too
because of this video I've just realized that I might have a mutable default problem in one of my private libraries. Thanks!
The real issue with `import *` is not shadowing in the way you showed, because you can understand that kind of shadowing statically from your environment.
The real issue is actually that you may be deploying your code in an environment where each module has different versions, and if they are using semantic versioning then *adding a new feature* to those modules only bumps the *minor* version, which is assumed to always be backwards compatible. If anything changes that is not backwards compatible, the module would have bumped the *major* version instead, and package managers on the deployment end will use this standard to automatically get the most up-to-date but still compatible version of the dependent modules.
However, if you use `import *` then this new feature will be imported into your program, possibly shadowing part of another module that you could not have possibly known about at the time you wrote the code, which turns industry standard backwards compatible updates into automatic code breakage!
Isn't this why we have docker images?
In case someone still doesn't know, R language does not have the import as syntax at all, so functions from various libraries often override each other. You often need to use syntax like base::mean(), which means using the mean function from the base library.
The dummies behind tidyverse have created some tools, like forcing users to explicitly specify the library origin for each function when namespace conflicts are detected. It's just replacing one nightmare with another nightmare.
R is used a lot in my field. I do my best to avoid it like the plague.
One reason for that probably comes from using S3 objects. Because everything is an object, typically all classes implement a global function like `print`, `summary` etc that dispatches when called on that instance. So it’s almost a feature, not a bug to have a global function. But, for some reason we have another OOP system called S4, and another one called reference classes… and another one called R6. The big problem is a lot of it is not standardized, and probably the biggest thing is it’s trying to force too much structure that it becomes too awkward. For example, you can filter from base R and filter from tidyverse, which both do different things but dispatch on a `data.frame` object. Tidyverse technically creates `tibble` objects, which makes a nice separation, but doesn’t force you to only use `tibble`s, and you can `dplyr::filter` on `data.frame` objects. So yeah, it’s complicated haha. I love R and I find it’s very flexible and expressive, but the lack of standardization can be hard to navigate for beginners which is exacerbated by the fact that most non programmers use R.
Btw the `box` library is the solution to the `library()` awkwardness you mentioned, it’s heavily inspired by pythons import syntax and creates a module abstraction to compliment the package abstraction python has, in R.
@@samw1248
Thank you for your detailed reply! I completely agree with your points. The lack of standardization in R, especially with the multiple OOP systems like S3, S4, and R6, can be quite challenging, particularly for beginners. Your explanation about the global functions and the complications arising from using different libraries like base R and tidyverse really resonated with me. The `box` library sounds like an excellent solution to the `library()` awkwardness. I'll definitely look into it. Thanks again for sharing your insights!
@@timelikewater1988 happy to help!
Rstudio actually gives you explicit warnings when any function is overshadowed by another when importing, so it's pretty convenient.
I don't mind "else" with "try" since it would naturally follow an "except".
Unless they've changed this behavior, you _can't_ have it without an "except" even though you can have a "try" without an "except" ("try . . . finally"). Thus, it's really "except . . . else", because either "except" or else "else".
@@MAlanThomasII Exactly. So it actually makes sense in that context.
It makes sense to me because i think of except as "if exception." I never knew it worked with loops though, that behavior is weird.
@@Fanta666 This. The alternative is `except` being replaced by `if except`, though the suggested alternate syntax of "noexcept"/"nobreak" is also an agreeable compromise.
@@MAlanThomasII What would you use "try...else" for? If this were valid syntax, I would just remove it because there's no difference between "try: print(1) else: print(2)" and "print(1); print(2)". Don't be silly.
Copying like that is an issue in a lot of languages. In Rust for example, Vec would deep-copy, while Rc would be a shallow copy. And you don't always know from the outside what the fields of a type are, and therefore what x.clone() will do.
Same in C++ with vector vs. shared_ptr.
I don’t think it’s fair to say Python’s string literal juxtaposition causes concatenation is “poorly thought out”, because this was a feature of C. In C, it made more sense in the context of macros and automatically generated code. And Python has borrowed a lot of other syntax from C, so at the time *not* having this feature would’ve been more conspicuous.
C doesn't have a string concatenation operator, Python does. Python breaks with tons of C traditions (it's one of very few who put bit operators &, | above comparisons ==, > etc in the priority table!) - and it has a philosophy of 'one correct way', so making the + concatenation optional goes against its core values.
@@sharpfangThe "one correct way" has been broken many times; it is merely a preference. It is in no way to be followed.
PEP 584 directly addresses this.
"In practice, this preference for “only one way” is frequently violated in Python....We should not be too strict about rejecting useful functionality because it violates “only one way”."
The language "violates" the philosophy when there's a good functionality, multiple times throughout its history.
@@moho472 Except this functionality generates very hard to catch bugs, so it's very arguable if it's a good functionality.
@@sharpfang That could be said for every single language, and is not unique to Python.
I do agree, implicit string concat is just unnecessary, and it kindof forces one to use a linter to catch stray cases like strings inside a list, for the expense of code looking a bit nicer in some cases.
This topic is very close to my heart. I love Python as a programming language but I have faced these issues. Since I code in multiple languages, I have been gravitating more towards syntactically rigid languages.
Way back when python first started catching on, there were some variants which added back in (optional) typing, blocks denoted by curly braces, ect. I liked that. But alas, most folks didn't...
The lack of strictness is a bit of a tradeoff between ease for small stuff and scripts, and making it harder for large/complicated things. However, the real brilliance of python IMO is being able to fairly easily include lower level C and C++ code as modules. It also beats the hell out of perl
@@travcollier Python is almost as bad as Excel in that people keep abusing it for stuff it was never intended to do.
One thing close to copies is when you try to initialize a 2d array like this: a = [[0]*5]*5, it wouldn't do a proper 2d array (an array with multiple different arrays in it), but an array with multiple references of the same array, so if you were to go a[0][0] = 1, it would change the first elements in all of the rows, not only the first one
This made me screw up a leetcode problem
Oh dear, I did not know that... although I think the only time I ever used that, was when I was creating a numpy array, which I'm pretty sure creates a deepcopy.
Luckily the list comprehension for this isn’t too hard; it’s just [[0]*5 for _ in range(5)]
this is good, because lists aren't arrays, and you should not be using them as arrays. Use an array, otherwise you are violating POLA.
@@DrDeuteron well, what are arrays in python?
11:55 if the editor or IDE was to feature disambiguation of imports, could that present a code execution vulnerability given how flexible python module definitions are? At that point, developers would need to read the source of modules for verifying the lack of possiblly malicious code execution (on top of the normal benefits and tradeoffs)
I think shallow copies make more sense than you'd realize. Generally speaking you want to copy the least ammount of memory possible and be very explicit over deep copies.
@@craftylord3336
It kinda makes sense when you realize that all these languages work with reference types and primitive types. A reference-type is just a pointer to the real data, so when you copy the shallow copy is justified by this same reason (the pointer is copied).
You generally don't want deep copies when you have this kind of reference-type structure as it would blow your memory and also your GC, and because most of the time what do you need is a shallow copy.
@@craftylord3336
You use = to set these types that are what I so called primitive types. And even if they were not primitives it would still make sense because their memory layout is entirely flat.
When you write the number 100 the number is fixed and cannot be changed, every single bit required for it's identity (which is value-based) is already here, so you use = to set it to another number (the same for the other ones).
Also, about the function called copy vs function called "pointer" I don't get it, no single language has a function called "pointer" because it does not make sense, a pointer is just a reference to an object somewhere in memory, underlying it is a number (like an 'int') so when you call 'copy' you are really copying everything that is flat there (including this int), just not the pointed object itself which is on another place in memory.
If you want a language that does deep copies of your lists use some that don't have reference types like C, C++ or Rust, most modern languages that have references (including C#, Kotlin, Java, Dart, JS and Python) suffer from the same thing you called a "poor design" that in reality just makes sense if you think about it for a minute.
@@craftylord3336= does copy the reference, not the object. Test it with the `is` operator!
I disagree with the shallow copy vs deep copy part. That would be really unexpected coming from any other GCed language. How does it even handle ref-loops? Haven't tried Python's deep copy, am on a phone, but does it crash, fill up memory, somehow track all objects and try to recreate the loops? How does deep copy handle custom objects? There's no one way to copy objects in Python, there is no copy interface/protocol last time I checked. Seems like a deep copy function would be full of hacks.
@@craftylord3336 a isn't a reference to b, a is a reference to 2 after the assignment. No object gets copied with assignments, only references.
@@craftylord3336 Python uses variable length integers. For small values I assume it uses some tagged pointer optimizations, but speaking in the general case int values are (immutable) objects allocated on the Python heap. Tagged pointer optimizations are implementation details and semantically it still behaves the same.
Big hater of implicit string concatenation - recently caused a large amount of calculations to silently not run for me
What?! No mention of package dependency management? :)
6:34 i feel like there are some scenarios where "Exception" makes sense, tho. i often find myself running into scenarios where 2-3 different exceptions can occur in a try/catch block, but i intend for the program to handle any of those exception types the same way (a good example could be a method that expects an integer as a string, and should do the same thing in the event that the input is either null or a letter). why waste space with redundant code, or risk forgetting a specific exception type that could occur there, when you could just catch all of them and handle them all accordingly ? in that specific case, I don't see that as being lazy or careless, but rather as being thorough to make sure the code doesn't crash for users
I think what the video creator meant was that an exception shouldn't be broader than it needs to be. Obviously, if you _do_ wish to catch all exceptions, then Exception is the way to go.
An addition to the star imports:
Not using star imports also benefits to the speed and the file size of your application. If you use a big library like PySide6 (for creating GUIs) and you import everything, your compiled app will be roundabout 200-300 megabytes. If you only use the Widgets, Gui and Core (which most applications do), then you will end up with like 20 megabytes and a MUCH better startup time AND in addition to that it also helps your IDE, as it doesn't have to index dozens of docstrings and functions.
But if you only use small libraries like colorama it doesn't really matter, but still a good habbit to not do star imports :)
My most terrible feature is that a += b and a = a + b can have different semantics in Python if a and b are objects.
Such as builtin arrays
@@KirkWaiblingerWait, don't they do the same thing on built in arrays?
@@jogadorjnc
a += b is equivalent to a.extend(b), which mutates a in place rather than creating a new list
a = a + b creates new list and assigns it (the behavior that both should do)
🤮
@@KirkWaiblinger Wait, why would you ever want the 2nd one? Isn't it always slower and more memory hungry?
Also, thinking about it now, it kind of makes sense, the += operator is doing the same thing, but taking advantage of its use being more restrictive to be more efficient
Edit: Maybe I'm just too OOP-pilled at this point, I just see objects and methods with fancy shortcuts to use them
@@jogadorjnc you might well want the extend behavior... in which case, you should write a.extend(b) ("implicit is better than explicit" and all that). Having a += b not equivalent to a = a + b is nuts.
The trouble is, you can go a long time not realizing that they have different behavior until you get some subtle bug due to having mutated a reference you passed somewhere. And a += b is pretty far down on your list of things that might be suspicious when debugging the problem
to the star imports:
Namespaces are one honking great idea -- let's do more of those! - the zen of python
Implicit string concatenation is great, it allows to do multiline strings but without losing the indentation, it's a feature I'd wish to see in some other languages
The last feature is quite expected. After all, a list in the memory of a computer is just a pointer to a memory address :)
Actually I mostly write code in php and its copy-on-write behavior was confusing me for a long time in the past.
Regarding splitting a long string over mutliple lines, i was gonna say i would write this in a triple quoted string and then unfold it with some stdlib tools. But when i looked for such tools i realized that neither textwrap nor shlex have them ready to go. You would have to do it in two steps (like textwrap.dedent().replace("
", "") ) or use a full on regular expression. Or i guess you could make a proper tuple of your string lines and use str.join(). There's a lot of option, but they all seem like a lot (ish) of work just to make your source code prettier :)
damn, that target = [] "feature" gotta be the dumbest thing ever
Great video, thank you. I am mainly a C# and JS dev but like to see what other languages share/do differently.
What about else with 0 runs? So while (false) else print('yes') or no?
Why would you use "else" in a real example? Just to avoid checking count/state afterwards like if count < length / isAllGood = false?
wow using else in a for loop as a sort of confirmation that everything had looped through successfully was something I would have never have thought of.
though it does make a lot of sense in a while loop. I literally used it in my very first python script to close off a loop. It just seemed very intuitive to me since while is a sort of logical statement.
It also makes sense in a try block. Less obvious but still it makes total sense.
Actually all of them do make kind of sense if you just build a regular sentence out of them like:
- if condition is true do a thing else do another thing
- while condition is true do a thing else do another thing
- try to do a thing, if it breaks do another thing, else do a different thing
- for current thing in collection of things do a thing, else do another thing
for is definitely the most confusing one though. Especially with break although it does make sense if you understand what break breaks you out of the current block entirely and else is part of that block so it won't be run if interrupted by a break.
My favourite mis-feature is the syntax for single-element tuples. When moving around code I often wind up with something like
channel_id = "indently",
and then trip up later as channel_id is now a tuple, not a str.
I personally use for else in my code, but i you are also right, it doesn't justify for what it actually means. I used to use from module import * but then i got to know the importance and i don't use it. And btw i never knew the difference between shallow copy and deepcofee until i watched this video 😅
I know when you're using default mutables for a dataclass it requires you to use a function that returns the mutable to get around this, would that work in an ordinary function call as well? I don't think it's any easier to read than the boilerplate you have, but it would be a different way of doing it
import * and mutable defaults are both caught by pylint, at least. But yeah these aspects of Python all have sharp corners.
Also, when did the | syntax for type hints show up? I use typing.Optional and typing.Union since I wasn't aware of that bit of syntax sugar.
To represent one piece of data of multiple types using type hints in Python 3.10 or newer, you can use the pipe operator (|).
I thought that you goinng to say, that else is worst feachure bc you can mistakenly make else not for if, but for for,
like:
for i in range(10):
if i == 5:
print(five)
-else:-
-print(i)-
_else:_
_print(i)_
and you get an error
Messing up your nesting is just a skill issue
@@SonOfMeme It's always baffled me how people will complain about the whitespace in Python, but then if you don't use whitespace "properly" in their language of choice they bitch about it.
Vestigial semicolons and meaningless whitespace... why?
I also don't like how enums work, the fact that you have to call auto() for each of them when in 90% of applications you are going to use auto anyway. Why not make that the default, while still allowing overwriting with a value in the rare cases you need a specific value.
Also having to import packages for such basic features always seemed a bit hacky. Almost like its not part of python but you had to rely on someone else's implementation.
I am starting to learn Python and even if the subjects are more relevant to people who already have mastery of it, I found it very interesting to follow the video (by reproducing the examples, because I learn better by doing it even if it's shown, that way I can test a little more)
And I already liked seeing certain practices often seen in tutorials which could go against the good practices that you mentioned and therefore avoid getting into bad habits and in addition I learned some things with the video that I I don't know enough about it yet but it will probably be useful to me one day.
You see, try, except, else works for me.
I would agree with you however that in the case of 'for' and 'while', it does seem unintuitive... but, hey, at least I learnt something more about looping!
😊
the 'else' in 'for' and 'while' I would expect means 'if there were no elements reached by the loop' which is nearly the opposite of what it actually means
@@MagicGonadsThe idea is that you'd often loop to find some particular element. If you find it, you break out of the loop and continue from there. But if you reach the end of the iterator... Well, now, you need to do something else. This something else in case of this failure, you'd put in the else block, knowing it's only ran if you failed to break out of the loop.
@@gJonii but semantically 'for all of these things, otherwise this' is what a construct 'for-else' would mean intuitively, is what I'm saying. For loops may often be a search, but not every for loop is a search.
Yeah, its very confusing in the for and while loops, and even for the try/except i feel like its not even worth it. A "nobreak" or even a good old "then" would make it much clearer
But the whole thing could be much less ambiguous by explicitly setting a boolean variable (e.g. found, error, etc) before the loop and changing that variable in the same line as the break/exception, then using an if after the loop to explicitly run some code if the variable was changed.
You don't need a new keyword for every possible scenario, or else we'll end up with a "noop" keyword for when the loop is iterating over an empty list or something
@@sutirk yeah my interpretation of how 'else' would work would also be called 'empty' (the case in which the iterator is empty) and often you just handle this explicitly
the else block is like the exact opposite of what you would think
it doesn't even make sense compared to how it works with if
if anything it should run only when broken out
i think it shouldve been named 'also' block
The use case presented for it is element search. You loop over an iterator, searching for some element. If you find it, you'd have "if element == target: do stuff; break"
But now you'd write code after the loop. Can you trust you've found the element? Perhaps not. Perhaps your loop just ended naturally, and your cool break logic never ran. What to do then? How would you even know that happened? Enter else-block. It's only ran in this scenario, so you know your break-logic was never ran.
You'd have absolutely no benefit from this also-block that runs if broken out from loop, since you could put this logic manually to the "if condition: break" section for much more readability.
FYI if you want to run code when a for loop is broken, the way you would do that is to put the code before the break. Something like:
for x in xs:
if x is None:
print("Got unexpected value, breaking loop")
break
else:
print("Processed all values successfully")
You can also kind of see how it DOES make sense with the if. In this example, which is how for...else is usually used, the "else" only runs if the "if" never runs. In expanded form, the above code translates to something like this:
if xs[0] is None:
...
elif xs[1] is None:
...
elif xs[2] is None:
...
else:
print("Processed all values successfully")
@@gJoniithat makes sense but it’s weird to me that python cares about this very niche use case but doesn’t have named breaks to allow breaking out of multiple nested loops. Rust lets you break out to any scope you want by name and even “return” a value with your break statement which can be used to solve this problem too.
I mean I get it, python is much older and is full of tons of design decisions that we wouldn’t choose again knowing what we know now. But it’s just a bit frustrating when a “low level” language lets me often write higher level code than a “high level” language.
The shallow- vs deepcopy is new to me. What would be a useful use case for a shallowcopy?
Mostly to drive people away from the language.
You'd almost always want to use a shallow copy on a list containing immutable data. Like a list of strings:
A = ["1", "2", "3"]
B = a.copy()
B[1] = "c"
print(A) # ["1", "2", "3"]
print(B) # ["1", "c", "3"]
Strings are immutable in Python, so you never have to worry about the pitfalls of modifications to B propagating to A. This means that A and B require less memory to store than if B deep copied A, because they both have the same references to elements 0 and 2. So only 2 new objects have to be created (B and "c"). A deep copy would require 5 new objects be created (B, "1", "2", "3", and "c").
When you want the elements in the list to be reference identical. Perhaps they're being used as dict keys, or will share mutations.
About the “shallow copy” issue. This sort of thing is a major caveat of working with super high level, high abstraction languages. Not only you’re not expected to manage memory, it is actually hidden from you. The notion that a list is accessed by either a pointer to a contiguous block of memory or a pointer to the head of a linked list is missed by many. When you write “a = [ 1, 2]”, the variable “a” is not the list itself, it is not automatically aware of the data 1 and 2. “a” is just a reference to the list. This is what you’re copying around by default.
does the shallow copy method not do the exact same thing as just setting a_copy = a? Since in both cases the variables both point to the exact same data if I'm understanding correctly. So then why is there an explicit copy method when it does the same as just assignment, you would expect it to do more than that, such as actually deep copy
What I'm understanding is that list.copy() DOESNT fully copy the nested list. It only fully copies the outer layer elements, and creates a reference to the inner list (which comes from the same memory location as the original list) and thus, editing the nested list of the copy created with the list.copy() method would actually be altering the original nested list that is referenced in the copies list.
@@valerielboss yes, so a_copy[0] *= 2 will also change a[0]. One way to test is evaluate the boolean:
>>>a is a_copy
False
which compares the id()'s.
So in his 1st example:
>>>a_copy == a, a_copy[1] is a[1]
(True, True)
while:
>>>a_copy is a
False.
(but I didn't test those in an interpreter, but all pyhtonistas should try it out to learn it).
It gets easier to understand the behavior if you understand memory and pointers/references.
In his example, you have
a = [1, ["a", "b"], 2]
What that actually means is (in some very generic pseudocode notation):
mem0 = ["a", "b"]
pointer0 = address(mem0)
mem1 = [1, pointer0, 2]
a = address(mem1)
So what "a_copy = a" does is that both variables point to the same memory address:
a_copy = address(mem1)
In this case both a and a_copy are exactly the same, they point to the same memory and have the same content.
The shallow copy "a_copy = a.copy()" copies that region of memory along with every *value* in it:
mem2 = [1, pointer0, 2]
a_copy = address(mem2)
In this case, a and a_copy have exactly the same content, but one of its values is a pointer to another object, so both a and a_copy share this object, and when this object changes it will affect both a and a_copy.
Finally, the deep copy "a_copy = deepcopy(a)" actually goes into every pointer and copies that memory region too, saving the new memory addresses in the list:
mem3 = ["a", "b"]
pointer3 = address(mem3)
mem4 = [1, pointer3, 2]
a_copy = address(mem4)
In this case, a and a_copy have the exact same values, but are completely independent, whatever you change in one will never affect the other, because a references pointer0, whilst a_copy references pointer3.
instead of memorizing deepcopy to be imported from copy, can't I just use new_list = [element for element in old_list if element in old_list] ? ie a list comprehension. I think that is easier overall than memorizing a whole class and method narrowly defined as deepcopy.
So you'd rather cleanup fails in your code because you didn't realize that some library that used another library that used another library didn't document one of the errors that can be raised deeper in the call stack, right?
At least catch an Exception and log if the type of exception is unknown, unless you're absolutely sure the function raises only what you know it raises.
Or unless your code doesn't have any important cleanup to do.
If I make a shallow copy, is there any way to display the list that displays the references so that I _know_ I'm dealing with a shallow copy? (I figure this might be useful in debugging.)
map everything into `id` if it's not a primitive
The else in try block makes sense to me as I've always understood it as "(if) except: ... else (no exception: ...
The else in the loops is less intuitive to me. It seemed to me like it should run if there were no iterations at all
Yeah I agree, the for-else blocks require you to think of a for loop as a series of checks for which "breaking" is the sign you've found what you're looking for. Most people are taught that loops are for doing something, and break is for when you want to stop doing that thing early...which is kind of conceptually the opposite.
I don't read it as applying if there were no interactions at all though, I read it as "if any of them failed (had a break)"
it does run the else block if the loop had no iterations. It's useful if say you count successes:
for count, item in enumerate(container, start=1):
break
else:
count = 0
print(f"Found {count} items)
w/o the else block, you get a NameError, and to prevent that you would need to predefine count=0, which is U G L Y, and unpythonic.
Regarding the mutable defaults, when you fixed it why did you write “target | None = None”? Inside the function you checked to see if target was None anyway, so why not just make it “target = None”?
It's the appropriate type annotation according to the docs.
The fifth example caught me out causing hours of trying to figure out what I’d done wrong! I had no idea there was such a thing as a shallow copy. It was only after asking for help on a forum that I understood what was happening.
From a Python beginner:
• Are there any benefits of using deepcopy vs a_copy = a[:]?
• There’s no need to import when using a[:].
• Could this syntax be a fairly new addition?
a[:] also returns a shallow copy
@@IndentlyAs said, I’m a beginner, but would there be any benefit in the supplied example (17:41)? The contained list - [‘a’, ‘b’] - is hard coded. I understand that there would have been a difference if the list in the variable “a” were to have contained another list variable.
Example:
a = [1, 2]
b = [a, 3]
b_copy = b[:]
Then b_copy would, in my understanding, be affected by changes in a, but not by changes in b, nor b be affected by changes in b_copy.
By the way, thanks for your informative videos.
That's correct. The slice operator creates a new object with shallow copies of the objects in the sublist you picked. Shallow copies of basic data types are just a new copy of the data. Shallow copies of complex data types are not (Technically it's a new copy of the pointer pointing to the object, but then I'd have to talk about pointers).
@@bjorn_ It depends on what the type is of the value you're operating on in any given list.
When you make a shallow copy of a list, you create a new list containing references to the same elements held by the original list. This means that if the original list contains primitive types (like integers or strings), they appear to be copied. But in reality, the new list simply points to the same memory locations. If the original list contains mutable objects (like lists or dictionaries), these are not copied; both the original and copied list refer to the same objects. So, if you modify a mutable object in one list, the change is reflected in the other.
On the other hand, when you make a deep copy of a list, you create a new list and also create new copies of every item contained in the original list. This includes creating copies of all mutable objects. So, if you modify an object in one list, it does not affect the other list.
Here's an example:
from copy import deepcopy
# Original list
a = [1, 2]
b = [a, 3]
# Shallow copy
b_copy = b[:]
b_copy[0][0] = 'x'
print(a) # Output: ['x', 2]
# Deep copy
a = [1, 2]
b = [a, 3]
b_deep_copy = deepcopy(b)
b_deep_copy[0][0] = 'x'
print(a) # Output: [1, 2]
15:35 can someone explain to me(very green and curious programmer) why not to declare this list like a = [1, [a, b], 2]? Tbh didn`t even know that you can declare variables in python like you did
These are type hints, they are relatively new to the language and optional. I'm pretty sure that they are not enforced at runtime, but the editor can use them to point out mistakes that you might have made.
There should be a video about highly desirable missing features in python. Great vid though
Dude got some serious issues with Bob 🤔
5:31 If I made the language, I would just implement this differently. With an if statement, else only runs when the if statement did not run, so I would make for and while do the same thing: the else only runs if the loop is never initially triggered. Essentially:
while(Initially true statement): while code runs
else: else code does not run, no matter how few times the while code ran
while(Initially false statement): while code does not run, not even once
else: else code does run
for i in (Some data): for code runs
else: else only runs if there was no data to start the for loop
I think this is more useful because not being able to start a for loop or while usually means 1 of these 3 things happened:
1. we hit a base case in some kind of recursive code, which might need code that only runs in the base case
2. we had no data to process, so we might need to load in default data, or respond to the empty data in some particular way
Which developer’s version of Python do you recommend ?
Which version has the fewest inherent 🐞 🐛 🐜 bugs ?
What about global and nonlocal?
Hey, fun video! What editor are you using? It looks like VSCode but not quite... I'm really curious.
It's PyCharm from Jetbrains
11:49 Does no one make a ruff extension for pycharm
17:53 a_copy = a is the same as a shallow copy
list(a) is the same as a deep copy
Both of those statements are false, and you can verify it by using what you wrote in a Python script.
I'd encourage anyone who posts information to check it before sharing it on the internet.
13:55 um, does Python have a spread operator? In JS, if you want to guarantee a new list, you'd use the spread operator: [...target, name].
Shallow copies are spain without the p
sain?
A trip abroad where you aren't allowed to use the toilet?
without the “s”?
obviously a programmer as they made an off-by-one error
@@ShunyValdez shallow copies are:
>>>func = functools,.partial(filter, 's'.__ne__)
>>>"".join(*func(''Spain'.casefold()))
'pain'
is safer. Why index?
Number 1: The interpreter
Most modern versions of Python are not interpreted, but a hybrid between interpreted and compiled. They're compiled to byte code on the fly
@@iso_2013 Sounds like interpreter talk to me
@@tenv sounds like a JIT compiler. Still garbage tho.
Excellent video! Very useful. 🤯🔥👋
regarding star imports: pylint will throw various warnings at you about them, such as to not use them in general, but it also has a lint for star imports shadowing each other.
pylint, with a lot of configuration about which issues you care about, is actually a very nice tool to use in any project. there really isn't an alternative for it yet.
Pylint extension in VSCode does pick up the default = [] mutable issue.
It reports "Dangerous default value [] as argument"
14:32 the if statement creates unnecessary branching, which could make the function run slower. a better way to do it is `target = target || [];`.
forget about speed, just reducing cyclomatic complexity is a win.
the else block for while is kinda weird.. the else block for the "for" loop is extremely unintuitive, since I would expect "for i in items .... else:
I always import the entire module/package instead of importing single functions, is this bad practice? I prefer to access copy.deepcopy() than to access it as deepcopy() because if someone is reading or glancing at the code they will think deepcopy is an independent module/package
8:15: I always thought this construct was called "finally", particularly in a Try block.
11:30 I never liked star importing even though it was constantly referenced in Python documentation. Even as a novice programmer (hey, I'm still a novice) I always thought there was a danger of conflicts and confusion by bringing everything into the same namespace.
17:45 This is the best demonstration of deep versus shallow copying I've ever seen. Now I understand it. And this is definitely completely unintuitive behaviour.
Today I was trying to do some scripting in Excelscript; the darn thing is a nightmare, mostly due to some incredibly questionable choices in the API that makes everything far more complicated than it needs to be. Your Python complaints are comparatively mild compared to my complaints about Excelscript.
Thanks for the useful video; I learned some things. For implicit string concatenation, that comes in very handy when needing to do deeper escaping of strings containing a mix of single and double quotes. Used right, it's a huge help. I agree that used wrong, it's a mess. Maybe best to not use it unless it's actually needed. Great video overall, though. Thanks!
I agree with you when it comes to "else" blocks on loops, but try/except/else actually makes perfect sense to me (first try this, then if there is an exception do this, otherwise do this), and it can be very useful, especially in cases where you want to do something with a result that won't be there in the exceptional case, without accidentally catching errors you didn't mean to. I'd even go so far as to say that if you find yourself writing more than a single function call inside a "try" block, you should consider whether some of it should go in an associated "else" block instead.
Good list learnt something new today.
1.) Didn't know this, can't really see a use for it and can see how that would be annoying.
2.) Didn't know this either, could be useful.
3.) Did know this, but never use star imports personally.
4,) Bit by this before, when my editor didn't warn me. I spent hours trying to figure out why something wasn't working.
5.) Come up against this before but don't think it's too bad.
I'm totally with you with the first 4 features, but the last one do you have in any language i know, because of the reference type of the nested list (or to be clearer in python because of the mutable type, because in the end everthing is a reference type in python). Therefore, copy behave as expected in my opinion. What would be nice on the other hand, an additional deepcopy method for example.
Doesn't work that way in C++.
@@isodoubIetC++ is a lower level language where you are usually preoccupied with memory management and performance. In higher level and usually interpreted languages it's much more common to see pass-by-reference as the default, at least for object types. That would include JavaScript and consorts, PHP, Ruby, C#, Java,... Problem is, it always comes with an overhead, usually either reference counting, garbage collection or both, because you have to keep track of where the object is still needed or not. That's not an acceptable tradeoff for a systems level language like C++ or Rust, but you can always implement your own if you so desire.
@@eldonad It has nothing to do with C++'s focus on performance. It's just a conscious design choice based on the idea that it's much easier to reason about programs where your objects behave just as the built-in types.
@@isodoubIet Ok, I've thought about it for a bit, and I can imagine a weird version of C++ where objects are passed by reference by default, so I stand corrected. However I still think passing by value as a default is more natural in runtimes with unmanaged memory, since in that case specifying the flavour of reference you use can provide you with information you wouldn't care about in a garbage collected runtime. But eh, at the end of the day every language is kind of pass by value at heart, only that the value can be a magic handle to an object, or a shared_ptr...
I think the string concatenation wasn't explicitly thought out, but brought from c, as python is compiled to C. In C, you can do the same thing with strings.
Did not know about the mutable inputs. Good to know!!
In the Ruby programming language, the role of the "else" keyword, as described in the second section, is performed by the "ensure" keyword. I think it's a much better name. It's also slightly different, because the "ensure" code block is always executed.
Honestly the mutable defaults issue is easily the worst thing here.
I have never once seen anyone have the issue with missing commas in a string list.
I don't understand at all why `try:`, `except:`, `else:` doesn't make sense to you. It comes right after the `except`, so, it's `else, if no exception`.
Absolutely valid criticisms for the string concatenation and shallow copies.
There is also a problem when you're trying to make a list with multiple copies of the same thing (ie. lista = [[item1], [item2]] # if you do [[item1] * 2, [item2]] * 2 and then try to adjust item 1, it will adjust all of the first elements of all of the copies of lista. There is a way around it, but to find out the easy solution you have to go to the Q&A section of the documentation -_-
I disagree on you star imports point, if you are making a function that is already defined... I feel like you're setting yourself up for failure! Why would you do that?!
But yeah, good video.
I don’t understand why python breaks function scope like this. Any new call to the function should also make the default parameter values different memory locations from previous calls. Or better yet, once the function call ends all the internal variables can be marked for GC and ignored by the interpreter
The deepcopy can yield unexpected behaviors when it acts on objects without recursive memory calls, which may be another reason for why it's not default. I don't know what the cause of these unexpected behaviors are, but I've run into situations where performing a deepcopy on an object makes it unusable, while performing a shallow copy works perfectly. My guess is that more complex integrated objects are more likely to have internal parameters that you don't want to copy, and so are more likely to want to be shallowly copied instead of deepcopied.
I think the worst feature is the copy, maybe is made it with shallow copy because Python itself is heavy. But I think they should change to specific copy like a.shallow_copy() instead of only copy method.
List comprehension usage would be useful for a lot of these vs what you do like in mutable example . Or using inline defaults
Time and performance gains too
I mean... For try/except/else, the else is after the exceptions. So it's like, "else, if there are no exceptions, do this". Makes sense to me.
For for loops, it makes even more sense. You break out of the loop because you are done with whatever you were doing. If you reach the end of the loop, you're probably not done, so you fall through into the else block.
Best example is searching for a particular element. Once you find it, you break out of the loop. If you reach the end, you didn't find it.
It's also strange that shallow copy can be created by calling a method of lists with `list.copy()` whereas deep copy requires to be done with `deepcopy(list)`, which is imported from another module called `copy`😧...
I've been trying to get back to programing Python more intensely after many years; and one of the things that has been bothering me is it has way too many ways to do the same thing to different things, but it's not always done the same way for different types sometimes; and it can get even worse when using libraries that come with their own additional types...
Calling the else branch of the for or while loops a success is somewhat questionable. One could consider the break statement execution to be a success instead, actually. For example, it might mean we found something we looked for.
0:23 I prefer things like "now" followed by "here".
1:34 Whoops! :-)
5:05 Cool, it's a C for-loop. :-)
16:03 Why not b?
My least favorite thing in python is the bytes() constructor because it has one notable inconsistency with the str() constructor that is inconsistent with the other constructors in the same space. Here's an example:
A = "1"
int(A) # => the integer 1
str(int(A)) # Now we've roundtripped back to the string "1"
A = "1"
bytes(int(A)) # this is b"\x00", ie the null byte.
Unlike the str() constructor which turns an integer into a decimal string representation of the number, the bytes() constructor creates a byte string with as many null bytes as the integer specified.
bytes() is explicitly made to work with ASCII text, why would you pass in an int?
I assume that passing an int works as a handy way to get x number of NUL bytes because otherwise it would be incredibly ambiguous.
In your case, should bytes(int("1")) be parsed as 1 in hex (\x01) or as the string "1" (\x31)?
What if we pass in bytes(int("111"))? Do we expect it to give us the character "o" (\x6f) or the character "1" three times (\x31\x31\x31)?
I guess you can see how it would be useless either way because you're either limited by only outputting the bytes 1-9 over and over again; or your input would have to be made of a concatenated mess of a bunch of decimal values for characters making a truly meaningless int, and which would be even more ambiguous to parse if you consider multiple characters, and then extended ascii and encodings like UTF-8...
Your first question can be answered with a similar question: str() is explicitly made to work with abstract text, why would you pass in an int?
bytes(int("1")) => b"1"
bytes(int("111")) => b"111"
Rationale:
int(b"1") => 1
int(b"111") => 111
For non-ascii::
int(u"一") => ValueError, only characters 0-9 are recognized so bytes doesn't have to handle that either.
My view on this in general, which should address your arguments that I did not explicitly mention above: mapping an int to bytes *is* ambiguous but it is the same amount of ambiguity as mapping an int to a str and mapping bytes to ints. The decision as to which of the possible outcomes Python will use for those values has been made. So for roundtripping with int and symmetry with str(), bytes() should have been implemented with the same choice.
I had never any problems to remember using else with the try statement. You always have to get the idea of a language, it doesn't matter if its a spoken language or a programming language. Python reduces the reserved words like "else" by using them in a slightly different context with several statements or using well known statements from other languages in a different way. So "try" is a special use case "if" to handle exceptions instead of logical expressions, the "for" loop is in fact a special case of the while loop with an implicit exception handling. I never thought about it in a negative way, sometimes it took me only some time to really understand the idea of the statement.
"else" with while and for loops works completely different than other uses of the "else" keyword, the behaviour doesn't really have anything in common, that's why it's bad
The thing that makes it worse, is that in if else blocks, else gets ran when the if condition is false, whereas else gets ran when a try or while loop finishes successfully. Now yes, you could make the argument that else gets ran when the while condition returns false, but that's not the way people think about it.
@@b4ttlemast0r The else makes sense if you think about how it is implemented. A while loop would be something like this (Yes, this is a mix of asm and python):
:loopstart
# your loop content
if loop_cond:
goto loopstart
else:
# your "else" code
:loopend
Now you imagine "break" as "goto loopend".
It is similar for try:
# try-block
if error_happened:
# except block
else:
# else block
# finally block
I hope you can understand what I mean, this is how I memorize it :)
What is your addons list?
Well, use a linter 😸
It scolds you for star-import and non-specific exceptions.
Maybe it can warn about implicit concatenation (or you can instruct it to).
And IDE usually shows some warnings.
Yeah, that's all well and good, but a language should be intuitive without a linter. Having to rely on a linter to remember how to do unintuitive stuff feels like having to walk around with a textbook on English grammar just to go to the grocery store down the street.
@@oliver_twistor The problem with programming languages is that they instruct computer to do what is written, not what was intended.
For the grocery store, a "point and click^H say 'this one'" interface is usually enough :)
I often use a loop to search for something, and use "break" to break out of the loop when it is found. Then "else" means it was not found, which kind of makes sense to me.
This is unrelated to your section on .copy(), but wouldn’t it be more straightforward to just make a copy of the mutable defaults in the first line of the function?
target_copy = target.copy()
target_copy.append(name)
return target_copy
no, because the code path for default and not are different, when the user provides a list it actually does want to mutate it
Would be weird to have a default in that case, but technically possible
I think the most irritating part about else block is that for "if" statement it means that "if" *did not* work
We will not stand for this Bob hatred! Very interesting video, especially perplexing for me as I'm more familiar with Java.
Sometimes, for-else block is very useful!
he agreed, his only 0xDEADBEEF was the name in the for/while context. I do love the construction, and have no problem with the name...for me it's "BREAK or ELSE"....
Totally agree on the else for the reason; if you don't go into the if you go into the else. So it would make intuitive sense if you don't go into the loop block you go into the else rather than its present logic. And there is far more cases where it would be useful to use else if you cant loop, rather than if you can.
That else block to me (java developer) seems quite unintuitive, it is a success-block complete-block? Why name it else (other than the fact that it already is a keyword I guess)?
I think I've seen "try...else" in other languages too. So I wouldn't have an issue understanding it. But on the other hand: What's the purpose? You don't really need it at all. Might be handy with loops but within a try block you can just put the "success" as the last statement.
Nice video !
Usually i put inside the try: some lines to be executed after de dangerous code, if nothing triggers an exception that code will execute, otherwise it won't... so i don't really get the purpose of the else: at all...
If the line you thought was dangerous doesn't throw, but a line afterwards does, you may be catching different behavior than you expect. So using the else can help you be precise about what you know how to catch and handle vs what should propagate an error