2

I'm not sure if this is possible but is there a way to change a string that a function prints while calling it from another function? I want to do something like this:

def string():
    print ("This cat was scared.")

def main():
    for words in string():
        str.replace("cat", "dog")
        # Print "The do was scared."

main()
cs95
  • 379,657
  • 97
  • 704
  • 746
Kade Williams
  • 1,041
  • 2
  • 13
  • 28
  • 3
    why not pass an argument? – RomanPerekhrest Mar 14 '18 at 06:39
  • The supposed duplicate of this question was literally asked one hour after this was posted. Can people at least check the dates before flagging things? – Michael Smith Mar 23 '18 at 13:36
  • 3
    Correct, this post is what lead to the other being posted. – Kade Williams Mar 24 '18 at 03:38
  • 1
    @MichaelSmith It was closed because the top voted answer here was exactly the answer there. And since OP hasn't said a _word_ about what it is they really wanted, I could do nothing but assume they were really asking the same thing. – cs95 Mar 28 '18 at 23:03
  • 1
    Also, there's nothing wrong with closing an older question as a duplicate of a newer one provided the questions are identical and the newer question is "more informative" (subjective) than the older. – cs95 Mar 28 '18 at 23:04

3 Answers3

4

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()
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • This probably doesn't belong here, especially since there's now [a separate question specifically about it](https://stackoverflow.com/questions/49271750/hijacking-print-statements-in-python/49272080#49272080), to which I posted mostly the same answer. Should I delete this one? – abarnert Mar 14 '18 at 07:44
  • 2
    Tbh I'd just delete the other answer after including its information here. You could then edit this answer to point to the separate question for anybody interested in more hacky approaches. – Dimitris Fasarakis Hilliard Mar 14 '18 at 12:22
  • Can you keep it here for future reference? I'm not going to use it for this but I find it interesting. – Kade Williams Mar 14 '18 at 14:54
3

As a guess:

  • You wanted string() to return a value the caller can use, instead of just printing something to the screen. So you need a return statement instead of a print call.
  • You want to loop over all of the words in that returned string, not all the characters, so you need to call split() on the string.
  • You want to replace stuff in each word, not in the literal "cat". So, you need to call replace on word, not on the str class. Also, replace doesn't actually change the word, it returns a new one, which you have to remember.
  • You want to print out each of those words.

If so:

def string():
    return "This cat was scared."

def main():
    for word in string().split():
        word = word.replace("cat", "dog")
        print(word, end=' ')
    print()

main()

That fixes all of your problems. However, it can be simplified, because you don't really need word.replace here. You're swapping out the entire word, so you can just do this:

def main():
    for word in string().split():
        if word == "cat": word = "dog"
        print(word, end=' ')
    print()

But, even more simply, you can just call replace on the entire string, and you don't need a loop at all:

def main():
    print(string().replace("cat", "dog"))
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • what is that `print()` doing? – Vicrobot May 10 '18 at 06:14
  • 1
    @Vicrobot It prints a newline. Since we were printing each word in the loop with `end=' '`, they’re all on one line, and the cursor is still on that same line, so we want to move to the next line. – abarnert May 10 '18 at 06:16
  • Then why we need to give other value to `end` parameter? – Vicrobot May 10 '18 at 06:21
  • 1
    @Vicrobot Because otherwise, each word would print on its own separate line, with newlines between them. This way, they all print on the same line, with spaces between them (and then we print a single newline at the end). – abarnert May 10 '18 at 06:22
1

What I think you may actually be looking for, is the ability to call your function with a default argument:

def string(animal='cat'):
    print("This {} was scared.".format(animal))

>>> string()
This cat was scared.

>>> string('dog')
This dog was scared.

If you pass nothing to string, the default value is assumed. Otherwise, the string prints with the substring you explicitly passed.

cs95
  • 379,657
  • 97
  • 704
  • 746
  • 1
    Not _sure_ that's what he wants, but it looks like a good guess, and if so, it's a good explanation. – abarnert Mar 14 '18 at 06:44
  • @COLDSPEED he maybe looking for this, but what if I can't change the definition of `string()`function and it is what is. Is there a way to replace strings passed in print functions of the function that I'm calling? – moghya Mar 14 '18 at 06:48
  • 2
    @moghya Not without some stealthy trickery, that's something that's beyond my expertise and beyond what I would recommend someone to do... – cs95 Mar 14 '18 at 06:49
  • 1
    @moghya There are a couple of ways to do that, but they're all very ugly, and should never be done. The least ugly way is to probably replace the `code` object inside the function with one with a different `co_consts` list. Next is probably reaching into the C API to access the str's internal buffer. If you want more explanation, create a new question—but again, you never want to actually do this; it's just interesting if you really want to understand Python under the covers. – abarnert Mar 14 '18 at 06:54
  • @abarnert Wow... If you were asked to code this up, could you do it?! – cs95 Mar 14 '18 at 06:59
  • @cᴏʟᴅsᴘᴇᴇᴅ Sure. Here's [modifying a string's internal buffers](https://github.com/abarnert/superhackyinternals). The other one, I don't think I have lying around, but it would only take a few minutes. – abarnert Mar 14 '18 at 07:03
  • @cᴏʟᴅsᴘᴇᴇᴅ [I'l probably regret this, but…](https://stackoverflow.com/a/49271683/908494) – abarnert Mar 14 '18 at 07:14
  • @abarnert I turned my query into a [question](https://stackoverflow.com/q/49271750/4909087). Please feel free. – cs95 Mar 14 '18 at 07:19