All of your
if char == '>': ptr += 1 and similar checks should use
elif after the first check. By using
if for all of the checks, you're forcing them all to run, even once you've found a match. This is wasteful because the checks are necessarily exclusive of each other. Once one check is true, none of the other checks can be. For example, you should have:
if char == '>': ptr += 1
elif char == '<': ptr -= 1
elif char == '+': values[ptr] = (values[ptr] + 1) % 256
elif char == '-': values[ptr] = (values[ptr] - 1) % 256
Now the checks stop once a match is found which prevents unnecessary equality checks.
I'd also try to break this down into a few functions to help testing. Right now, you can only test
bf_interpreter as one whole. You could have a function that takes the current character, and the state of the program (the
outputs...), and returns a new state. That way you can easily test for a given state if a certain command produces a correct new state.
I'm assuming this loop is just to add padding so you don't go off the end of the slots?:
while length <= ptr:
length += 1
You could make that a little neater by just using math and some concatenation. You could also just get rid of
length and use
needed = ptr - len(values)
values +=  * needed
ptr - len(values) calculates how many slots are needed, then
 * needed produces that many
+= adds them to
needed is negative,
 * needed will produce
, and essentially cause no change.
If you want to avoid the temporary list that
 * needed creates, you could replace that with:
values += (0 for _ in range(needed))
+= just pulls from a generator that produces values as needed.
And then, just like how you don't need
length, you don't need
len(code) is fine;
len runs in constant time. You don't need to cache it for performance reasons.
Here's some timings to show the difference in runtime this can cause:
TEST_CODE = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
>>> timeit.timeit(lambda: bf_interpreter_orig(TEST_CODE), number=int(2e5)) # Two hundred thousand tests
77.3481031 # Takes 77 seconds
>>> timeit.timeit(lambda: bf_interpreter_len(TEST_CODE), number=int(2e5))
bf_interpreter_orig is your original code, and
bf_interpreter_len is your original code but using
Yes, there's a difference. Note though, that's a ~11 second difference across 200,000 calls. That works out to roughly 58 microseconds per call to the interpreting function.
Unless you're calling
bf_interpreter hundreds of thousands of times in a tight loop, the difference is unlikely to matter. This also likely has nothing to do with the fact that you're requesting a length, and more to do with one extra function call. Function calls aren't super fast in Python. Likely any extra call to any function would have similar effects.