By popular demand (well, one person's curiosity…), here's how you actually could change the string in a function before calling that function.
You should never do this in practice. There are some use cases for playing around with code objects, but this really isn't one of them. Plus, if you do anything less trivial, you should use a library like bytecode
or byteplay
instead of doing it manually. Also, it goes without saying that not all Python implementations use CPython-style code objects. But anyway, here goes:
import types
def string():
print ("This cat was scared.")
def main():
# A function object is a wrapper around a code object, with
# a bit of extra stuff like default values and closure cells.
# See inspect module docs for more details.
co = string.__code__
# A code object is a wrapper around a string of bytecode, with a
# whole bunch of extra stuff, including a list of constants used
# by that bytecode. Again see inspect module docs. Anyway, inside
# the bytecode for string (which you can read by typing
# dis.dis(string) in your REPL), there's going to be an
# instruction like LOAD_CONST 1 to load the string literal onto
# the stack to pass to the print function, and that works by just
# reading co.co_consts[1]. So, that's what we want to change.
consts = tuple(c.replace("cat", "dog") if isinstance(c, str) else c
for c in co.co_consts)
# Unfortunately, code objects are immutable, so we have to create
# a new one, copying over everything except for co_consts, which
# we'll replace. And the initializer has a zillion parameters.
# Try help(types.CodeType) at the REPL to see the whole list.
co = types.CodeType(
co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
co.co_stacksize, co.co_flags, co.co_code,
consts, co.co_names, co.co_varnames, co.co_filename,
co.co_name, co.co_firstlineno, co.co_lnotab,
co.co_freevars, co.co_cellvars)
string.__code__ = co
string()
main()
If that's not hacky enough for you: I mentioned that code objects are immutable. And of course so are strings. But deep enough under the covers, they're just pointer to some C data, right? Again, only if we're using CPython, but if we are…
First, grab my superhackyinternals
project off GitHub. (It's intentionally not pip-installable because you really shouldn't be using this except to experiment with your local build of the interpreter and the like.) Then:
import ctypes
import internals
def string():
print ("This cat was scared.")
def main():
for c in string.__code__.co_consts:
if isinstance(c, str):
idx = c.find('cat')
if idx != -1:
# Too much to explain here; see superhackyinternals
# and of course the C API docs and C source.
p = internals.PyUnicodeObject.from_address(id(c))
assert p.compact and p.ascii
length = p.length
addr = id(c) + internals.PyUnicodeObject.utf8_length.offset
buf = (ctypes.c_int8 * 3).from_address(addr + idx)
buf[:3] = b'dog'
string()
main()