Do you obsess about writing your code just the right way before you get started?
Maybe you have some ugly code on your hands and you need to make it better.
Either way, refactoring could be your ticket to happier days.
On this episode, we'll walk through a powerful example of iteratively refactoring some code
until we eventually turn our ugly duckling into a Pythonic beauty.
Connor Hoekstra is our guest on this episode to talk us through refactoring some web-scraping Python code.
This is Talk Python to Me, episode 275, recorded July 9th, 2020.
Welcome to Talk Python to Me, a weekly podcast on Python, the language, the libraries, the ecosystem, and the personalities.
This is your host, Michael Kennedy.
Follow me on Twitter where I'm @mkennedy.
Keep up with the show and listen to past episodes at talkpython.fm.
And follow the show on Twitter via at Talk Python.
This episode is brought to you by us over at Talk Python Training.
Python's async and parallel programming support is highly underrated.
Have you shied away from the amazing new async and await keywords because you've heard it's way too complicated
or that it's just not worth the effort?
With the right workloads, a hundred times speed up is totally possible with minor changes to your code.
But you do need to understand the internals.
And that's why our course, Async Techniques and Examples in Python, show you how to write async code successfully as well as how it works.
Get started with async and await today with our course at talkpython.fm/async.
Connor, welcome to Talk Python to Me.
Thanks for having me on.
Excited to be here.
I'm excited too.
It's going to be beautiful, man.
Hopefully.
Yeah.
It's going to be a beautiful refactorings.
So I am a huge fan of refactoring.
I've seen so many people try to just overthink the code that they're writing.
They're like, well, I got to get it right.
And I got to think about the algorithms and the way I'm writing it and all this stuff.
And what I found is you don't really end up with what you want in the end a lot of times anyway.
And if you just go in with an attitude of this code is plastic, it is malleable, and I can just keep changing it.
And you always are on the lookout for making it better.
You end up in a good place.
Yeah, I completely agree.
Refactoring is not a one-time thing or something that happens only, you know, two years from when you initially write the code.
I heard once actually that refactoring goes a lot in hand with legacy code.
And there's a number of different definitions for legacy code.
But one definition is legacy code is code that isn't actively being written.
So if you write something once and then you consider it done, and then the next week, like no one's working on it, that technically, according to that person's definition is legacy code.
So that can be refactored.
You know, you can refactor something you wrote earlier in the day.
It doesn't have to be a year later or 10.
Yeah, absolutely.
I mean, you just, you get it working, you know a little bit more, you apply that learning back to it.
And with the tooling these days, it's really good.
It's not just a matter of, you know, if you go back to 1999 and you read Martin Fowler's refactoring book, he talks about these are the steps that you take by hand to make sure you don't make a mistake.
And now the steps are highlight, right click, apply, refactoring.
I mean, that's not 100% true.
And the example we're going to talk through is not like that exactly.
But there are steps along the way where it is, potentially.
Definitely.
Linters and static analyzers are heavily underutilized, I feel.
And so many of them will just automatically apply the changes that you want to do.
And it's fantastic for huge code bases.
It would be almost impossible to do it by hand.
Yeah, absolutely.
It would definitely be risky.
So maybe that's why people sometimes avoid it.
Now, before we get into that, though, let's start with your story.
How do you get into programming into Python?
I know you're into a lot of languages.
We're going to talk about that.
But Python too?
Python also?
Yeah.
So the shorter, it's a long story, but the shorter version of it is my degree in university,
which wasn't computer science, required at least two introductory CS courses.
So the first intro course was in Python.
The second one was in Java.
And then I ended up really, really enjoying the classes.
I ended up taking a couple more, but ultimately stuck with the career that I had entered into,
which was actuarial science.
That's like insurance statistics.
Yeah.
So you were in some form of math program, I'm guessing?
Yeah, yeah.
Yeah, cool.
It's very, very boring to explain.
But if you like math, it's a great career.
Yeah, awesome.
And so I ended up, for my first job at a university, I ended up working at a software company, basically,
that very simply explained, created the insurance calculator that many insurance companies use.
And after working there for about four or five years, I had just fallen in love with the
software engineering side of my job and had decided that I wanted to transition full time to like a purely technical company.
So it's several years or a couple years later.
And now I work for NVIDIA as a senior library software engineer.
And that's how I got into programming.
And our code base that we work on is it's completely open source and primarily uses C++14 and Python 3.
That's where Python enters.
That sounds like a dream job.
Yeah, that sounds awesome.
Yeah, I absolutely love it.
Yeah, so you're working on the Rapids team, right?
Which works on doing a lot of the computation that might be in Pandas, but over on GPUs.
Is that roughly right?
Yeah, that's a great description.
So yeah, within NVIDIA, I work for an organization called Rapids.
We have a number of different projects.
So specifically, I work on CUDF.
That is C-U-D-F.
So the CU is two letters C-U from CUDA, which is like the parallel programming language that NVIDIA has made.
And the DF stands for data frame.
And so this is basically a very similar library to Pandas.
The difference being that it runs on the GPUs.
So sort of the one liner for Rapids is it's a completely open source, end-to-end data science pipeline that runs on the GPUs.
So if you're using Pandas and it works great for you, like there's no reason to switch.
But if you run into a situation where you have a performance bottleneck, CUDAF can be like a great drop-in replacement.
We don't have 100% parity with like the Pandas library, but we have enough that a lot of Fortune 500 companies that pick up and use us are able to very easily transition their existing code in Pandas to CUDAF.
Right.
Change an import line, go much faster, something incredible like that.
That's the goal.
That's the dream.
Yeah, I just recently got a new Alienware, a high-end Alienware desktop.
And it's the first GeForce I've had in a long time that's, you know, not like, I don't know, some AMD Radeon in a MacBook or something like that.
So I'm pretty excited to have a machine that I can now test some of these things out on at some point.
Yeah.
Acceleration on different devices is, it is very exciting.
Awesome.
All right.
Well, let's start by introducing real briefly a little bit about refactoring.
We've talked a tiny bit about it in general.
And then we're going to dive into a cool example that you put together that really brings a lot together.
And what I love about your example is it's something you've just gone and grabbed off the internet.
It's not like a contrived, like, well, let's do this and then unwind the refactorings until it does it.
It's like you just found it.
And like, well, let's see what this thing does.
That's going to be fun.
But let's just start with a quick definition of refactoring.
Maybe how do you know when you need it?
How do you know when you need refactoring?
For me, I have a sort of number of anti-patterns in my head that when I recognize them in the code, some people might refer to them as sort of technical debt.
This idea that the first time you write things or maybe initially when you write things, you don't have the full picture in mind.
And then as time goes on, you start to build up technical debt in your code.
And a refactoring can be reorganizing or restructuring your code or rewriting little bits of it to basically reduce tech, to make it more readable, maintainable, scalable, and just in better, in general, better code.
That's sort of the way I think of it.
Yeah, it is pure sense, right?
It should not change the behavior, at least in terms of like inputs, outputs.
Exactly.
Yes.
The easiest code to refactor is code with tests, whether that's unit tests or regression tests or any of the other number of tests that there are.
If you have a code base that has zero tests, refactoring is very, very dangerous because you can refactor something and completely change the behavior and not know about it, which is not ideal at all.
Somewhat suboptimal, indeed.
You know, Martin Fowler, when he came up with the idea of refactoring, or at least he publicized, I don't know, I'm sure the ideas were basically there before.
One of the things that struck me most was not the refactorings, but was this idea of code smells.
And it's like this aesthetic of, right, like I look at the code and it, yeah, it works, but like your nose kind of turns out, you're kind of like, ew, no, ew, but it still works, right?
It's like not broken, but it's not nice.
And, you know, there's all sorts of code smells like too many parameters, long method, things like that.
But they rarely have clear cutoffs, right?
Like, well, if it's over 12 lines, the function is too large, but under that, it's totally fine, right?
Like that's not, it's never really super clear cut.
So I think this whole idea of refactoring, much like refactoring itself, requires like going over it and over it as sort of through your career to refine, like what the right aesthetic to achieve is.
And it probably varies by language as well a little.
Yeah.
If you start to do it like consciously when you're looking at code and asking yourself, like when you have that code smell feeling like something's not right here.
If you are conscientiously like paying attention to what it is, like slowly over time, you will start to pick up on exactly what it is about it.
Like a very, very small one for me.
And I think this is mentioned in maybe a clean code or it might've been Martin Fowler's book.
It's like declaring a variable earlier than it needs to be declared.
So you might declare like all your variables at the top of a function, but then like two of them you use immediately.
But the other three you don't use until the last, you know, four lines of the function.
Small things like that.
It seems simple, but I've made the change where I've put the declaration closer to where it's get used.
And then you realize, oh, wait a second, this isn't actually reference.
Like it's set to something, but then it's not actually used later on.
So I can just delete this.
And it's because it was at the top of the function.
You can't see where it's being declared or if it's used somewhere else that like you actually just have an, a phantom unused variable that can be deleted.
It's simple things that lead to better changes later on.
Well, and just mental overhead.
Like you said, the technical debt side of things.
So for example, there's the variable that was at the top.
Literally when the code was written, it was being used, but it's been modified over the years.
And now no longer is it being used, but because it's separated from where it's declared to where it's used.
You don't want to mess with that.
Like if you start messing with that, you're earning more work, right?
You're asking for more.
I'm just going to make the minor change.
I don't want to break anything.
Who knows?
And then the next person that comes to try to understand it, they got to figure out, well, why is there that like set count variable?
Like, I don't feel like it's being used, but it's there.
And like, you know, you just got to, it's another thing to think about that's in the way.
Yeah, for sure.
Yeah.
So certainly I think it's viable.
There are fantastic tools that will like highlight this variable is unused or this assignment.
Is it meaningless or something like that?
So there are options, but still it's better to not let that stuff live in the code.
Yeah.
A hundred percent agree.
Let's talk about this example that you've got here and maybe you should give a little background on your language, enthusiasm and programming competition interests and so on.
Your interest in coding competitions, I think it's probably worth touching on already.
But then this example is from you trying to reach out and understand it and do some analysis of those environments or those ecosystems, right?
What's the background with this, these different languages and coding competition?
Yeah, so I initially got into competitive programming, quote unquote.
So just the one sentence description is there's a number of websites online, HackerRank, LeakCode, CodeForces, that they host these one to three hour contests where they have three to four or five problems that start out easy and then get harder as you progress through them.
And you can choose any language you want to solve them in.
And the goal is just to get a solution that passes as quickly as possible.
So it's not necessarily about how efficient your code is.
It has to run within a certain time limit.
But if you can get it to run or pass in Python versus C++ versus Java, any code solution works.
I started doing these to prepare for technical interviews.
So if you're interviewing for companies like Google, Facebook, etc., a lot of their interview questions are very similar to the questions on these websites.
And so I at one point was looking for a resource online, like for YouTube videos that just explain this stuff.
But at the time, I couldn't really find any.
So I started a YouTube channel covering the solutions to these problems.
And I thought it would be better to solve it in a number of languages than opposed to just C++.
So I started solving them in C++, Python, and Java.
And that's sort of what led to my interest in competitive programming.
Even though I'm not interviewing actively anymore.
I just find these, they're super fun.
It keeps you sort of on your toes in terms of your data structure and algorithms knowledge.
And you can treat them as like code katas.
I'm not sure if you're familiar with the concept of just sort of writing one little small program and trying it a couple times in different languages.
And you learn different ways of solving the problem that you might not would have initially solved the problem that way.
This example, I decided to just figure out what are the top languages that people use to solve these competitive programming problems on a given website.
So the site that I chose was Code Forces.
Yeah.
And you're like, hey, I'm working on this new data frame library that's like pandas.
Let me see how I can use pandas to solve this problem and get some practice or something, right?
Yeah, yeah.
So when I had just started NVIDIA, I knew that the pandas library existed, but I had zero experience with it.
And I knew that it had this sort of group by reduction functionality that if you had a big table of elements, you could get these sort of statistics on, you know, what's the top language or what's, you know, the average time it takes for people to submit very easily with this kind of library.
So I thought, what better way to learn pandas than by trying to build a simple example that uses this library for something that I'm interested in.
And so the first thing that I did was I googled, you know, how to scrape HTML tables using pandas.
And then it brought me to this blog that at the end of the day has about 60 lines of code.
And it's a tutorial blog.
So it walks you through how to get this code off of an HTML table.
And basically the PyCon talk that I gave, it came out of doing this.
I had no plans of giving a PyCon talk on this.
I just after having gone through it and sort of refactoring one by one, I realized that like I could give a pretty simple talk to like Connor like five years ago.
That didn't know about any of the I didn't know about list comprehension.
I didn't know about enumerate.
I didn't know about all the different techniques I was using.
And I figured it would be at least for some individuals out there.
It would be a useful talk highlighting the things that I didn't know when I first started coding in Python.
But that now are like second nature for me.
And that's sort of where the talk came from.
Yeah.
And it's really interesting.
The example is cool.
I do think that a lot of the refactorings were let's try to make a more Pythonic version of this and more idiomatic.
version of this like misunderstanding the for in loop, for example, and treat them all right.
So in a lot of ways, it's a cool refactoring.
But it's also kind of leveraging more of the native bits of the language, if you will.
Absolutely.
Yeah.
So you went and grabbed this code and it does two basic things.
It goes and downloads some HTML and then pulls it apart using, I think, LXML HTML parser.
And then it's going to loop over the results that it gets from the HTML parser and turn this into basically a list or a dictionary.
Then you're going to feed that over to pandas.
Ask pandas some pretty interesting questions.
And most of the challenge or most of the messy code lived in this HTML side of things, right?
Yeah.
That's a pretty good description of what's happening.
Cool.
So let's go and just talk through some of the issues you identified and then the fix, basically knowing like how did you identify that as a problem?
And then what fix did you apply to it?
Now, there's a lot of code and it's hard to talk about code and audio.
So we'll maybe try to just like as high level as possible, talk about like the general patterns and what we fixed.
The first part of the code would go through and it would create an empty list and it would create like an index to keep track of where it was and then did a loop over all of the elements.
Increment the index, add a thing to the list, print out some information as it went, right?
Yep.
And I think the first thing that you talked about was the code comments, actually.
You're like, what is this code comment here?
It just says we're looping over these things.
Well, what do you think a loop is?
Why do we have this comment?
Yeah, even worse was like arguably the second comment some might argue is add some value.
But the first comment above the line that creates an empty list, it says create empty list.
And it's only a what is that six characters if you don't include the spaces.
And I think that's definitely one of the things that's called out in a number of refactoring books is comments should add value that is not explicitly clear from the code.
I think even beginners are able to tell that you're creating an empty list there.
There's no reason to basically state what the code is doing.
Typically, comments should say why if it's not clear why something is being done a certain way or something that's implicit and not explicitly clear from what the code is doing.
Yeah.
In terms of refactoring, I love this idea of these comments are sort of almost warning signs.
Because if I find myself writing one of these comments to make stuff more clear, I'm like, wait a minute, wait a minute.
If this is just describing what's here, something about what I'm doing is wrong.
Maybe the variable name is not at all clear what the heck it is.
Or maybe it could use a type annotation to say what types come in instead of here's a list of strings.
Like how about list bracket string goes there to just say what type it is.
It's Python 3 after all.
And, you know, from the Code Smells book, Fowler had this great description of calling these types of comments deodorant for Code Smells.
So there's something wrong.
It smells a little less bad if we like lay it out, set the stage.
But every time I see one of those, I'm like, you know what?
I just need to rename this function to like a short version of what this comment would say.
Or rename this variable.
Or like restructure and break these things apart.
Because if it needs a comment, it's probably just too complicated.
There's an individual in the C++ community.
His name is Tony Van Eerd.
And he has a rule, or not a rule, but a recommendation that you should grep your code base for step one, step two, step three.
And guaranteed you're going to get like one or two matches.
And a lot of times it's these steps of comments on top of pieces of code and like a larger function.
And odds are you could make that code a lot better by refactoring each of those steps into its own small function.
And just whatever the step, like if you put step one in a description, you've already given that piece of code a name.
You just need to take the next step, put it in a function and give that function that name.
Yes, exactly.
Which is exactly what you said.
Exactly.
I think there was even some tool way, way, way back in the early days of C# that if you would highlight some code to refactor it and you highlighted a comment, it would function nameify.
It would try to guess the function name by using the comment, turning it into a function, you know, like something that would work as a identifier in the language.
Anyway, it's totally a good idea.
So there's a couple of things going on here.
One is like, why is there a print statement?
Nobody needs this.
Once you take that out, though, you are able to identify this.
Well, let's take a step back.
First, if you have an integer and you're incrementing it every time through the loop so that it stays in sync with the index of the elements you're looping over, that's probably not the best way to do it, right?
Like Python has a built-in enumerate.
Yeah.
This is probably one of the most common things I see in Python.
Sadly, in certain languages, they don't have this function.
But in Python, it's right there built into the language.
And as you mentioned, it's called enumerate.
So you can pass whatever thing you're looping over to enumerate.
And that's going to bundle it with an index that you can then inline destructure into an index and the element that you were getting from your ranged for loop before.
So anytime you see an index, IDX or I or something that's keeping track of the index and that's getting it could be J.
Sometimes it's J.
Sometimes it's K.
X or Y if you're being really creative.
And yeah, like there is a built-in pattern for basically avoiding that.
And it makes me extremely happy.
Like it happens actually not just once in this piece of code, but twice where you can make use of enumerate.
And once you see it, it's very hard to unsee it.
But like I said, this was something that I learned enumerate from Python.
And this was not something that I knew of and I didn't learn in school.
So there's a lot of Python developers and just developers in many languages out there that I think they're just not aware.
And as soon as you tell them, I think they'll agree.
Oh, yeah, this is way better than what I was doing before.
Yeah.
You just need to be aware of it.
You know, you always run into these issues.
You've got to create the variable.
Then like, why is the variable there?
Then you've got to make sure you increment it.
Do you increment it before you work on that with the value or do you increment it after?
Is it zero based?
Is it one based?
All of these things are just like complexities that are like, what is happening here?
Like, what if you have a have an if test continue and you skip the loop, but you forget to increment it?
Like there's all these little edge cases.
And you can just with enumerate, you can say, you know, it's always going to work.
You can even set the start position to be one if you want it to go one, two, three.
Beautiful.
Yeah, that's a great point.
Yeah, there are use cases where you're going to run into bugs.
Whereas with enumerate, you know, at least you're not going to have a bug with that index.
Right.
It's always going to be tied to the position with the starting place the way you want it.
So yeah, yeah, that's really nice.
But it's not super discoverable, right?
Like there's nothing in the language that screams and waves its hands.
It says, yeah, you're in a for loop.
We don't have this concept of a numerical for loop, but this is actually better than what
this is what you wanted.
You didn't even know you wanted it.
Yeah, it has to be something that you stumble across.
Interestingly, some languages go is the one that comes to mind.
They actually build in the enumerate into their range based for loop.
So in Go, they have built in basically the destructuring.
And if you don't want the index, if you just want a range based for loop and you want to
ignore the index, then you're just supposed to use the underbar to say, I don't need the
index for this loop.
But it's interesting that like Go is a more recently created language than Python, at least.
When they decided like they thought it was such a common use case that they would think that
most people need it more often than they wouldn't.
So they built it into their for loop.
So with that language, you can't avoid learning about it because it's in their for loop.
It's a syntax error to not at least say I explicitly ignore this.
Yeah.
Oh, interesting.
I didn't know that about Go.
So now you've got this little cleaner.
You look at it again and you say, well, now what we're doing is we're creating a list,
an empty list, which we commented, create empty list.
That was cool.
Took that comment out, but it was very helpful in the beginning to help you understand.
No, just kidding.
And then you say we're going to loop over these items and then append something to that
list.
Well, that's possible.
But this is one of your anti-patterns that you like to find and get rid of, right?
This is an anti-pattern that I call initialize, then modify.
And actually, the enumerate example previously also falls into this anti-pattern.
So anytime you have a variable that it doesn't need to be a for loop, but many, many times it
is that inside each iteration of that for loop, you are then modifying what you just
initialized outside.
That is initializing and then modifying.
And my assertion is that you should try to avoid this as much as possible.
And when it comes to the pattern of initializing an empty list, and then in each iteration
of your for loop, you're calling append.
That is built in to the Python language as something that can be used as a list comprehension,
which is so much more beautiful, in my opinion, compared to just a raw for loop and then
appending for each iteration.
Yeah.
Every now and then, there's like a complicated enough set of tests or conditionals or something
going on in there that maybe not.
But I agree with you most of the time.
That just means what I really wanted to write was a list comprehension.
It is.
So, you know, bracket item for item in such and such, if such and such, right?
That's what you got to do.
Yeah.
List comprehension, once you start to use it, moving to a language that doesn't have it
makes you very sad because it's such a convenient syntax.
It totally makes you sad.
And I really, really wish list comprehensions had some form of sorting clause because at
that point, you're almost into like in-memory database type of behaviors, right?
Like I would love to say projection, you know, thing, transform thing, for thing in collection,
where the test is, order by, whatever, right?
I mean, you can always put a sorted around it, but it'd be lovely if they're like, it's
already got those nice steps.
I like to write it on three lines, right?
The projection, the set, and the conditional, like just one more line, put the order by in
there, but maybe someone, or maybe I should put a PEP in there.
Who knows?
I was going to say that sounds like a future pep, but.
It definitely does.
I mean, it would be easy to implement, just transform it to a sorted and pass that as a
key or something like that.
But anyway, it would be really cool.
But they're very, very nice, even without that.
And once you have it as a list comprehension, then it unlocks the ability to do some other
interesting stuff, which you didn't cover in yours because it didn't really matter.
But if you have square brackets there, and those brackets are turning a large data collection
into a list, if you put rounded brackets, all of a sudden you have a much more efficient
generator.
Yep.
That is something I don't call out at that point.
But at the end of the talk, I allude to a article that was mentioned on the other podcast
that you co-host, Python Bytes.
Yeah.
Thanks for the shout out on that one, by the way.
Yeah, no, it was a great article, but it mentions generator expressions right after it mentions
list comprehension.
And I mentioned that these things go hand in hand and that you should familiarize yourself
because if at any point you're passing a list comprehension to like an algorithm, like
any or all or something, you can drop the square brackets and then just pass at the generator
and it'll become much more efficient.
So it's good to know both of them.
And there's no way to go from a for loop really quickly and easily to a generator.
That's a good thing.
Right.
There's not like for yield, I and whatever, right?
Like there, but with the comprehensions, it's square brackets versus rounded parentheses, right?
It's so, it's so close that if that makes sense, it's like basically no effort to make
it happen.
Yeah.
Yeah, nice.
Okay.
So we've got into a list comprehension, which is beautiful.
And then you say, all right, it's time to turn our attention to this doubly nested for loop.
And it's going to go over a bunch of items.
And it's going to go over a bunch of the items and pull out an index and then, you know, go and work with that index.
So it's another enumerate.
And then I think another thing that's pretty interesting that you talk about, I don't remember exactly where it came in the talk, but you're like, look,
what you're doing in this loop is actually looping from like the second onward for all the items.
And that really is just a slice.
Yeah.
So in this nested for loop, the outer for loop is basically reads for J in range of one to the length of your list.
So you're basically creating a range of numbers from one to the length of your list.
And then right inside that for loop, you're creating a basically a variable that's the Jth element of your list.
So all you're doing is skipping the very first element of your list.
But the way you're doing this is generating explicit indices based on the range function and the length function.
And I thought at first that they must be doing this because we need access to the index later or we need access to our elements later.
But that wasn't the case.
It just seemed like the only reason they were doing all of this was to skip over the first element.
And so very nicely, once again, Python has very, very many nice features.
They have something called slicing where you can basically pass it the syntax, which is square bracket, and then something in the middle and square brackets.
And in order to skip the first one, you just go one colon.
Yeah, one to the end.
And that's beautiful because you don't even have to check the length of the items.
You just say go to the end, which avoids errors of like, do I have to plus one here?
Do I not?
Is it minus one?
Like, what is the ending piece?
But you don't have to worry about just from I skip the first one and the rest.
Yeah, it's so convenient.
You avoid making a call to Len.
You avoid making a call to range.
And you avoid your local assignment on the first line of your for loop.
You can basically remove all of that and just use slicing and you're good to go.
And yeah, slicing is slicing is a really, really awesome feature.
It actually comes from a super old language that was created in the 60s called APL.
And Python is one of the only languages that has something called negative index slicing,
where you can pass it a negative one so that it wraps around sort of to the last element,
which is a super, super, it sort of looks weird.
But once you use it, it's so much more convenient than doing like a Len minus one or something like
that.
It's it is a little bit unreadable.
But once you know what it does, it's great.
It's great.
It's like I want the last three.
I don't want to care how long it is.
I just want the last three.
And that's yeah, it's fantastic slicing, I think is fairly underused for people who
come from other languages.
But yeah, and it fits the bill because there's so many of these little edge case.
You talk about errors and programming, like off by one errors are a significant part of
problems programming, right?
And it just skips that altogether.
It's beautiful.
Yeah.
So the next thing to do is so you're parsing this stuff out of the Internet, which means
you're working with 100% strings.
But some of the time you need numerical data.
So you can ask questions like, is this the sixth or seventh or whatever?
And so they have this is going to be fun to talk about.
They have try value equals int of data.
So pass the integer has the potentially integer like data over to the int initializer.
Either that's going to work, or it's going to throw an exception, which case you will say
except pass.
Well, not you, the original article had that right.
So it's this try pars except pass.
Otherwise, it's going to be none or it's going to be set to the string value or something
to that effect.
So what do you think about this?
How do you feel when you saw that?
Yeah.
So my initial reaction was that this is four lines of code that can potentially be done
in a single line using something called a conditional expression.
So in many other languages, they have something called a ternary operator, which is typically
a question mark, where you can do an assignment to a variable based on a conditional predicate.
So something that's just asking true or false.
And if it's true, you assign it one value.
And if it's false, you assign it another value.
So in Python, they have something called a conditional if expression, which has the syntax assigned
to value using the equal sign, ask your question.
So in this case, we just ask, is it an int?
Or sorry, it's so the first thing that returns, it's actually backwards from ternary operator.
So the line reads data equals int of data if, and then check your predicate.
And in Python, we can just call is numeric on our value, which will return us true or false
based on whether it's a number.
So if that returns true, then it'll end up assigning to data int of data.
Otherwise, you can just assign it itself data.
And then it's not going to do any transformation on that variable because it's not numeric.
It's one line of code.
It's more expressive, in my opinion, and it avoids using try and accept.
And it's preferable from my point of view.
I would say it's probably preferable from my point of view as well.
I have mixed feelings about this, but I do think it's nice under certain circumstances.
One, for example, if you say try, do a thing, accept, pass, a lot of linters and PyCharm
and whatnot will go, this is too broad of a clause.
You're catching too much.
And you're like, okay, well, now to make the little squiggly in the scroll bar go away,
I have to put a hashtag disable check, whatever, right?
And I'm like, well, now it's five lines, one with a weird exception to say, no, no, this time
it's fine.
So that's not ideal.
I definitely think that it's more expressive to use this conditional if one liner.
The one situation where I might step back and go, you know, let's just do the try is if
there's more variability in the data.
So this assumes that the data is not none and that it's string like, right?
But if you got potentially objects back or you got none some of the time, then you need a
little bit more of a test.
I mean, you could always do if data and data is numeric.
That's okay.
But then it's like if data and is instance of string data and like there's some level
where there's enough tests that it becomes, you kind of like, fine, just let it crash.
Right.
And then we'll just catch it and go.
But we were talking before we hit record.
Also, like there's a performance consideration potentially.
Yeah, definitely.
And it's interesting.
I'll let you speak to what you found.
But on the YouTube comments of the PyCon talk, that was one of the probably the most discussed
things was whether or not the conditional expression was less performant than the original try and
accept because a couple individuals commented that it was it was more Pythonic to use the
try and accept and therefore it might be more performant.
But you can share with what you found.
Sure.
Well, I think in terms of the Pythonic side, like certainly from other languages, like say
C, C++, there's more of this.
It's easier to ask for forgiveness than permission style of programming rather than the alternative.
Look before you leap.
Right.
Because in like C, it can be a page fault and the program just goes poof and goes away.
If you do something wrong, whereas this, it's just going to throw an exception.
You're going to catch it or something like that.
So there's like this tendency to do this style.
But in terms of performance, I wrote a little program because I wanted to I'm like, maybe this
is faster.
Maybe it's slower.
Like, let's think about that.
Right.
So I wrote a little program, which I'll link to.
There's a simple gist.
I'll link to it in the show notes.
It creates one million, a list with one million items.
And it uses a random seed that is always the same.
So there's no, there's zero variability, even though it's random.
It's like predictable random.
And it builds up this list of either strings or numbers randomly, a million of them.
About two third strings, one third number.
And then it goes through and it just tries both of them.
It says like, let's just convert this as many of them as we can over to integers and do it
either with the tri-except pass or just do it with this is numeric test.
It is six times.
I got about 6.5 times faster to do the test, the one line test than it is to let it crash
and realize that it didn't work.
Yeah.
So there you go.
You heard it here on Talk Python to Me.
That's right, man.
Conditional expressions faster than try-except.
Talk Python to Me is partially supported by our training courses.
How does your team keep their Python skills sharp?
How do you make sure new hires get started fast and learn the Pythonic way?
If the answer is a series of boring videos that don't inspire or a subscription service you pay way too much for and use way too little, listen up.
At Talk Python Training, we have enterprise tiers for all of our courses.
Get just the one course you need for your team with full reporting and monitoring.
Or ditch that unused subscription for our course bundles, which include all the courses and you pay about the same price as a subscription once.
For details, visit training. talkpython.fm/business or just email sales at talkpython.fm.
I mean, there's a lot of overhead to throw in an exception and catching it and dealing with all that.
Now, right, this is a particular use case that varies and like all these benchmarks like might vary.
Like if you've got 95% numbers and 5% strings, it might behave differently.
Like, so there's a lot of variations, but here's an example you can play with in what seems like a reasonable example to me.
It's faster to do the is numeric test.
So a lot faster, right?
Not like 5% faster, but 650% faster.
So it's worth thinking about.
Yeah.
Yeah, for sure.
Let's see.
So come through.
And in the end, you had, I mean, a ton of stuff was here.
It was like 20 lines of code just for these two loops.
And now you've got it down to four lines of code by basically an outer loop and inner loop, grab the data and append it with this little test that you've got.
Much nicer.
I agree.
Yeah.
So you went, I think if you look at the overall program at this point, you were doing some analysis or like some reporting.
You said it started at 60 lines of code and now it's down to 20.
Yeah.
That's pretty good.
Roughly.
Depending on if you count, you know, empty lines and whatnot, but it was about 60 down to about 10 or 20 lines.
And at this point, I had sort of pointed out that I had made a mistake.
So like this was fantastic.
At least I had thought that, you know, I'd taken a code.
I'd taken a code snippet from a blog, reduced it by, you know, roughly 75% or 67%, depending on how you measure it.
But that I had made an even bigger mistake than I had realized.
And it was that when I had originally, I'd shown Googling for, you know, how to scrape HTML using pandas that I read the second results.
And the third result was actually what I should have chosen.
And it was that I had pandas actually has a read HTML method in the library.
And so the point that I go on to make, if you use that, you go from, you know, 10 or 20 lines down to like four lines of code.
And you're just invoking this one pandas API, read HTML.
And it's so much better.
So, you know, refactoring is fantastic.
But there's some quote about like the best code is no code.
If you don't have to write anything to do what you want to do and you can just use an existing library.
That's the best thing that you can do because that's going to be way more tested than the custom code that you've written.
It's going to save you a ton of time.
And you're going to end up with ultimately less code to maintain yourself.
And what's better than having someone else maintain the code that you're using for you?
Exactly right.
It gets better for no effort on your part.
Yeah.
It might get faster or it might handle more cases of like broken HTML or who knows.
But you don't have to keep maintaining that.
It's just read underscore HTML on pandas.
Just it's probably getting maintained.
Yeah.
And so like one of the things that I've echoed in some of the other talks that I've given is knowing your algorithms.
In C++, definitely there's a whole standard library.
There's a lot of built-in functions.
I guess they're not so much called algorithms.
They call them built-in functions in Python.
But like there's a whole page where I was just looking at it the other day.
And there's a ton of them that I'm just not aware of.
Everyone knows about map, filter, any all.
Like I just saw, I think it was called div mod, which was a built-in function for giving you both like the quotient and the remainder.
Which is like, there's definitely been a couple times where I've needed both of those and you do those operations separately.
And it's like, if I just knew about it, you can in a single line, you know, you can destructure it using the iterable unpacking.
Knowing your algorithms is great, but also knowing your libraries.
Knowing your collections.
Like the more you get familiar with what exists out there, the less you have to write and the more readable your code is.
Because if everybody knows about it, we have a common knowledge base that it's transferable from every project you work on.
Right.
Yeah.
Your final version basically had two really meaningful lines.
One was request.get.
The other was pandas.readHTML.
You don't have to explain to anyone who has done almost anything with Python what request.get means.
Like, oh yeah.
Okay.
So got it.
Next.
Right.
We all know how that works.
We know it's going to work.
And so on.
And it's really nice.
I think, though, what you've touched on here actually is really important, but it also shows why it's kind of hard to get really good at a language.
And the reason is there are so many packages, right?
You go to PyPI.
Let me try pyp.org.now.
Every time I go there, it's always more, right?
So 245,000 packages.
If you want to learn to be a good Python programmer, you need to at least have awareness at a lot of those and probably some skill set in some of them.
Because like Panda is one of those.
Request is another one, right?
The four-line solution that you came up with was building on those two really cool libraries.
And so to be a good programmer and effective means like keeping your eye on all those things.
And I just think that's, it's both amazing, but it's also kind of tricky because it's like, well, I'm really good with for loops and I create functions now.
You're like, great.
You've got 200,000 packages of study.
Go.
There's some quote that I've heard before where being a language expert is 10% language, 90% ecosystem.
And it's, you can't be a guru and insert any language if you don't know the tools, if you don't know the libraries.
It's so much more than just learning the syntax and learning the built-in functions that come with your language.
It takes years and it definitely doesn't happen overnight.
It's a challenge for all of us.
Yeah, for sure.
You know, maybe it's worth a shout out to awesome-python.com right now as well, which like has different categories you maybe care about.
And then we'll like highlight some of the more popular libraries in that area.
That sounds awesome.
That's a good one.
Yeah, for sure.
So you went through and you did nine different steps.
You actually have those called out very clearly in your slides.
You can get the slides from the GitHub repo associated with your talk, which I'll link to in the show notes, of course.
But all of this refactoring talk was really part of the journey to come up with a totally different answer, which was what are the most popular languages for these coding competitions?
Yeah.
Ultimate goal was to scrape the data and then to use pandas in order to do that analysis.
And at the end of the day, I believe the number one, I definitely know the number one language was C++ at about, I think it was 89%.
And that typically is the case because certain websites, they give the same time limit per language.
So a website like HackerRank, they vary by language.
So Python, your execution time that you're allotted is 10 times more for Python.
So even though Python's slower, they give you a proportionate amount of time.
But most websites don't do that.
So the CodeForces website, it gives like you, I think, two seconds execution time, regardless of the language you use.
And so due to that, most people choose the most performant language, which is C++.
But in second place was Python.
And I know a lot of competitive programmers that for the problems where performance isn't an issue that you're trying to solve for, they always use Python because it's about a fraction of the number of lines of code to solve it in Python than it is in any other language.
Sometimes you can solve a problem in one line in Python.
And the next closest language is like five lines, which is a big deal when time matters.
Yeah.
Are you optimizing execution time or developer time in this competition?
Right?
Yeah.
It definitely matters what you're trying to solve for.
So yeah, C++ was first.
Python was second.
Java was third.
And then there was a bunch of fringe languages.
The top three were C#, Pascal, and Kotlin.
And yeah, you can see a full list if you go watch the PyCon talk.
But it was cool, yeah, to find out what was used and what wasn't.
Yeah, it sure was.
And it was cool to see the evolution of what you created to answer that question, which is pretty neat.
All right, well, let's just talk a little bit about Rapids.
Because I know that people out there are, there's a lot of data scientists, and they're probably interested in that project.
So we did mention a tiny bit that it's basically take Handa's data frames, apply something like that, that API, pretty close, not 100% identical and everything, but pretty close.
And it runs on GPUs.
So why are GPUs better?
Like, I have a really fast computer.
I have a core I9 with like six cores I got a couple years ago.
That's a lot of cores, right?
So yeah, well, first thing I should highlight, too, is that Rapids is more than just QDF.
So QDF is the library I work on.
We also have QIO, QGraph, QSignal, QSpacial, QML.
And each of those sort of map to a different thing in like the data science ecosystem.
So QDF definitely is the analog of pandas.
QML, I think the sort of analog you can think of is like scikit-learn.
But also, too, like none of this is meant as replacements.
They're just meant as alternatives.
Like if performance is not an issue for you, like stick with what you have.
There's no reason to switch.
Yeah, don't do it because, for example, I couldn't run it on my MacBook, right?
Because I have a Radeon.
Right, right.
If you do want to try it out, I think they're on the Rapids.
So if you go to rapids.ai, we have a link to a couple examples using like Google CoLab
that are hooked up to like free GPUs that you can just take it for a spin.
And you need the hardware, but you can go try it out.
But like our pitch is sort of like this is useful for people that have issues with compute.
Right.
And for different pieces, you're going to want different projects.
So if you're doing pandas like sort of data manipulation, QDF is what you want.
But yeah, why are our GPUs faster?
It's just a completely different device and a completely different model.
So GPUs, typically it's in the G of the GPU.
We're known for being great for graphics processing, which is why it's called a GPU.
But at some point, someone coined the term.
He actually works on the Rapids team, Mark Harris.
He coined the term GPGPU, which stands for general processing GPU compute.
It's now typically referred to as just GPU computing.
But it's this idea that even though the GPU model is great for graphics processing, there
are other applications that GPUs are also amazing for.
The next best one is matrix multiplication, which is why they sort of became huge in neural
nets and deep learning.
But since then, we've basically discovered that there's not really any domain that we can't
find a use for GPUs for.
So there is a standard library in the CUDA model called Thrust.
So if you're familiar with C++, the standard library is called STL, and it has a suite of algorithms
and data structures that you can use.
Thrust is the analog of that for CUDA.
And it has reductions, it has scans, and it basically has all the algorithms that you
might find in your C++ STL.
And if you can build a program that uses those algorithms, you've just GPU accelerated your
code.
However, using Thrust isn't as easy as some might like.
And a lot of data scientists, they're currently operating in Python and R, and they don't want
to go and learn C++ and then CUDA and then master the Thrust library just in order to accelerate
their data science code.
The Rapids goal is to basically bring this GPU computing model for a sort of general purpose
acceleration of data science compute or whatever compute you want to the data scientists.
And so if they're familiar with the Pandas API, let's just do all that work for them.
Put the so so Rapids is built heavily on top of Thrust and CUDA.
And so we're basically just doing all this work for the data scientists so that they can take
their Pandas code, like you said, hopefully just replace the import, and you're off to
the races.
And some of the performance wins are pretty impressive.
Like I'm not on the marketing side of things.
But in the talk I mentioned, I just happened to be listening to a podcast called the NVIDIA
AI podcast.
And they had, I believe his name was Kyle Nicholson.
And by swapping out CUDA for Pandas for their model, they were able to get a 100x performance
win and a 30x reduction in cost.
That's 30 times, not 30%.
Yeah.
So 30,000 times, like multiplicatively, which is massive.
That's the difference between something running.
So if it's 100x in terms of performance, that's the difference between something running in 60
seconds or an hour and 40 minutes.
And if you can also save 30x, if that cost you 100 bucks, and now you only have to pay $3,
it seems like a no brainer for those individuals that are impacted by performance bottleneck.
Like I said, if you're hitting Pandas, and it runs in a super short number of seconds,
it's probably not worth it to switch over.
Yeah.
Well, and you probably, you tell me how realistic you think this is, but you could probably do
some kind of import, conditional import, like in the import, you could try to get the rapid
stuff working.
If that fails, you could just import Pandas as the same thing.
One is PD, the other is PD.
And maybe it just falls back to just working on regular hardware, but faster when it works.
What do you think?
That is definitely possible.
There's going to be limitations to it, though, obviously, if you have a sort of QDF data frame,
like I don't think you wouldn't be able to do it piecemeal.
But if you have a large product,
what I'm thinking is if you wrote it for the Rapids version, but then let it fall back to
Pandas, not the other way around.
If you take arbitrary Pandas code, you try to rapidify it, that might not work.
But it seems like the other one may well work.
And that way, if somebody tries to run it, they don't have the right setup.
It's just slower possible.
What do you think?
There's definitely a way to do that, to make that work.
It might require a little bit of, you know, some sort of boilerplate framework code that
is doing some sort of checking, you know, is this compatible else?
But like, that definitely sounds automatable.
Like, yeah.
Yeah.
That sounds cool.
Because that would be great to have it just fall back to like, not not working, just not
so fast.
Right?
Yeah, yeah.
The future of computing is headed to a place where we can dispatch compute to like different
devices without having to like, manually specify that, like, I need this code to run on the CPU
versus the GPU versus the TPU versus in the future, I'm sure there's going to be a QPU for
quantum processing unit.
Like, like, exactly.
Currently, we all think serially or most of us that don't work at NVIDIA, we think serially
in terms of like, the way that CPUs do compute, but I think in 10 or 20 years, we're all going
to be learning about different devices.
And it's going to be too much work to, in our head, always have to be keeping track of which
devices it's going to, at some point, there's going to be a programming model that comes out
that just automatically handles when it can go to the fast device, and when we can just
send it to the CPU.
Yeah, absolutely.
So just while you were talking, I pulled it up on that Alienware gaming machine I got.
It has a GeForce RTX 270, which has 2,304 cores.
So that's a lot.
That's a lot of cores.
And if you look somewhere, Google claims that it achieves 7.5 teraflops in the super, increases
that to 9 teraflops, which is just insane.
It's like a core i7 doing like 0.35, 0.28 or something like that.
So anyway, the numbers, they just are like, they boggle the mind when you think of how
much computation graphics cards do these days.
I think top of the line, I might get this wrong, but like the modern GPUs are capable of 15 teraflops.
It's an immense amount of compute that's hard to fathom, especially when coming from a CPU sort of way of thinking.
Yeah, absolutely.
Yeah, the only reason I didn't get a higher graphics card is every other version required water cooling.
I'm like, that sounds like more effort than I want for a computer.
I'll just go with this one.
Yeah.
All right.
Well, Rapid sounds like a super cool project.
And maybe we should do another show with the Rapid's theme across these things to talk a little bit more deeply.
But it sounds like a great project.
Glad you're working on it.
I work on the C++ lower engine of it, but I'd be happy to connect you with some of the Python folks that work on that side of things.
And I'm sure they'd love to come on.
Yeah, that'd be fun.
All right.
Now, before we get out of here, I've got to ask you the two questions.
If you're going to write some Python code, what editor do you use?
So I am a VS Code convert.
That's what I typically use day to day.
Nice.
Yeah, that's quite a popular one these days.
And then notable PyPI package.
Then you ran across like, oh, people should know about this.
Yeah.
So I like to recommend there's a built in standard library, which I'm pretty sure most Python developers are familiar with.
Itertools, which has a ton of great functions.
But less well known is a PyPI package called more hyphen itertools.
And I'm not sure if this one's been recommended on the show before.
But if you like what's in itertools, you'll love what's in more itertools.
It has a ton of my favorite algorithms, chunked being one of them.
You basically pass it a list and a number, and it gives you a list of lists consisting of that many things.
It's like paging for lists.
Yeah, yeah.
And there's tons of neat functions.
Another great one that's so simple, but doesn't exist built in all underscore equal.
It just checks, given a list, are all of the elements the same?
And it's a simple thing to do.
You can do it with all, but you have to check is every element equal to the first one or the last one.
So there's just a ton of really convenient functions and algorithms in more itto tools.
That's the one I recommend.
Yeah, that's cool.
And you can combine these with like generator expressions and stuff.
They are all these, you know, pull some element out of each object that's in there and generate that collection.
Ask if all those are equal.
And they go to all these ideas go together well there.
Yeah, they compose super nicely.
Yeah, for sure.
All right.
Final call to action.
People are interested in doing refactoring, making their code better, maybe even checking out Rapids.
What do you say?
I'd say if you're interested in what you heard on the podcast, check out the PyCon talk.
It's on YouTube.
If you search for PyCon 2020, you'll find the YouTube channel.
And yeah, if you're definitely interested in rapids.ai, check us out there.
I assume all this stuff will be in the show notes as well.
So maybe that's easier than YouTube searching.
Yeah, well, and then also you talked about YouTube channel a little bit.
Maybe just tell people how to find that.
We'll put a link in the show notes as well.
So they can, if they want to watch you, you know, talk about some of these solutions and these competitions.
Yeah.
So my online alias is code underscore report.
If you search for that on Twitter or YouTube or Google, I'm sure all the links will come up.
And yeah, you can find me that way.
Awesome.
All right.
Yeah, we'll link to that as well.
All right.
Well, Connor, thank you so much for being on the show.
It was a lot of fun to talk about these things with you.
Thanks for having me on.
This was awesome.
Yeah, you bet.
Bye-bye.
This has been another episode of Talk Python to Me.
Our guest on this episode was Connor Hoekstra, and it's been brought to you by us over at
Talk Python Training.
Want to level up your Python?
If you're just getting started, try my Python Jumpstart by Building 10 Apps course.
Or if you're looking for something more advanced, check out our new async course that digs into
all the different types of async programming you can do in Python.
And of course, if you're interested in more than one of these, be sure to check out our
everything bundle.
It's like a subscription that never expires.
Be sure to subscribe to the show.
Open your favorite podcatcher and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes, the Google Play feed at /play, and the
direct RSS feed at /rss on talkpython.fm.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Now get out there and write some Python code.
I really appreciate it.
Thank you.