Python - Not Quite Perfect … But Darn Good
For the last few years I’ve been working heavily in the Python programming language. It’s an absolutely wonderful language and I do love it so. Besides forcing good indentation practices and being otherwise good with whitespace, it also is a wonderfully clever little language with many neat tricks that a seasoned programmer can appreciate.
However, it’s not all sunshine and lollipops.
For being an interpreted language, most of it runs quite fast. And where its speed does fail, there’s always writing libraries in C++ and then wrapping it in Python to use in your Python code. So most of the time it has a wonderful performance if you know what you’re doing.
Just recently though, while working on some string code to remove ASCII characters 128-255 from strings, as these beyond-standard-ASCII characters don’t translate to Unicode, I am reminded of one of Pythons massive failings. It handles strings for sh_t.
Don’t get me wrong. Python has all sorts of neat methods and functions for string manipulation. It’s quite handy really. But it’s s…l…o…w. Something about the way that Python assigns a new string variable is just darn slow. And nearly every string operation requires you to assign a new string variable as each function / method returns a string that is your results. So when all of your functions return a new string, and creating new strings in Python is slow … you see how this is a very bad combination.
For example, the code I was working on. A really neat Pythonic way of doing things results in the function:
| def stripBadASCII1(contents): if not contents: return contents return “”.join([c for c in contents if ord(c) < 128]) |
This strange looking code goes through contents character by character and only joins the characters below ASCII 128 to the returned string. Simple. Neat. Effective. But keep in mind, the way Python works, for each new character joined to the string, it has to create a whole new string. And in Python this is molasses in January slow.
Now another method, a little more mundane in concept, goes like this:
| def stripBadASCII2(contents): if not contents: return contents for i in range(128,256): contents = contents.replace(chr(i), “”) return contents |
You see, the concept of this one is easier to follow. It replaces all bad ASCII characters in the string with an empty string, which effectively strips those characters from the string. Looping 128 times through the string for each bad ASCII character. Ugly. Messy. And yet, because it moves string contents in chunks instead of character by character, the number of new string operations is greatly reduced. So even though it loops 128 times through, where as the other function only executes once, it still manages to run faster for all of its seemingly slow 128 loops.
When I ran function 1, the character by character approach, through the contents of a test file typically used in the code this function is being written for, on average it took about 0.1 seconds.
When I ran function 2 through the same contents, using the seemingly slower 128 loop approach, it took on average 0.06 seconds.
Okay, so four hundredths of a second doesn’t seem like much of a difference. But the longer the string processed through these functions, the larger that difference will get. And more to the point, this my friends is how bad Python is at handling strings. A single pass through, character by character, is almost twice as slow as 128 loops through with chunks of characters processed together. Ouch. This certainly isn’t the C++ approach to strings.
So that’s one of Python’s major failings, is it’s string handling performance. Python has another major performance failing as well. Even though it can create and manage threads, it doesn’t ever use more than one processor. So if you have a multi-core CPU, you only use one core. If you have a multi-processor PC, it only uses one processor. If you have a dual CPU quadcore PC with a total of eight cores, it still runs on just one core of one processor. No matter how many threads your Python code contains. Ouch!
In fact, because of the overhead of thread management, and because it only runs on one CPU, Python actually runs slower the more threads you give it to handle. Yikes!
In the old days when multi-CPU computers were rare and multiple cores per CPU was unheard of, this was not a problem. Today is not those old days. Today two, three, and four core CPUs are common for desktops. Heck, I just built my hun’s new budget computer with a dualcore Celeron. So more than anything, this is really an issue Python needs to fix, and soon. They’ve put this one off far too long already.
A workaround is to try and develop your code as multi-process not multi-threaded. Or in other words break the program down into independent sub-programs that the main program can spawn as needed. There are memory sharing techniques that can be used for inter-process communication. It’s ugly. It’s messy. But being independent Python processes, they should spread across multiple CPUs/cores like a normal multi-threaded program would. Still, it’s very far from an ideal solution. It is just another messy workaround.
So that, my friends, is the down side of Python. It’s a wonderful language, but no language is perfect. They each have their own strengths and weaknesses, and Python is no exception.

