9

If I have this function, what should I do to replace the inner function with my own custom version?

def foo():
    def bar():
        # I want to change this
        pass

    # here starts a long list of functions I want to keep unchanged
    def baz():
        pass

Using classes this would be easily done overriding the method. Though, I can't figure out how to do that with nested functions. Changing foo to be a class (or anything else) is not an option because it comes from a given imported module I can't modify.

Paolo
  • 20,112
  • 21
  • 72
  • 113
  • Where is `bar` used? Is it used in the later functions (like `baz`?) You *would* want to replace it in those cases, right? – David Robinson Aug 11 '12 at 03:21
  • You can access various internaldetails of functione, because they are just objects. Use the dir function, and the language reference to find out more. – Marcin Aug 11 '12 at 03:48
  • 1
    I got it to work one level deep by hacking `func_code`, but would have to make my replacement smarter to allow calls to `bar` from `baz`. On the plus side, I haven't seen `SystemError: /Users/sysadmin/build/v2.7.2/Objects/cellobject.c:24: bad argument to internal function` in quite a while! – DSM Aug 11 '12 at 03:50
  • 1
    Do you have the source for `foo`? Maybe you can write your own version of `foo` and then monkeypatch it. – Joel Cornett Aug 11 '12 at 04:33
  • I'm with @JoelCornett. Why is it not able to be modified? Can it be copied? (If it's stored in something like GitHub you could even fork it). – David Robinson Aug 11 '12 at 06:20
  • @JoelCornett @DavidRobinson yes, I can have a copy of the original `foo`. Though, the main idea was to reuse the existing code without duplicate it. I prefer not to have to keep in sync my copy with the original, if this latter should change over time. From this POV M. Anderson's solution should work the way I want. – Paolo Aug 11 '12 at 10:41

2 Answers2

12

Here's one way of doing it, creating a new foo that "does the right thing" by hacking the function internals. ( As mentioned by @DSM ). Unfortunately we cant just jump into the foo function and mess with its internals, as they're mostly marked read only, so what we have to do is modify a copy we construct by hand.

# Here's the original function
def foo():
  def bar():
    print("    In bar orig")
  def baz():
    print("  Calling bar from baz")
    bar()
  print("Foo calling bar:")
  bar()
  print("Foo calling baz:")
  baz()

# Here's using it
foo()

# Now lets override the bar function

import types

# This is our replacement function
def my_bar():
  print("   Woo hoo I'm the bar override")

# This creates a new code object used by our new foo function 
# based on the old foo functions code object.
foocode = types.CodeType(
    foo.func_code.co_argcount,
    foo.func_code.co_nlocals,
    foo.func_code.co_stacksize,
    foo.func_code.co_flags,
    foo.func_code.co_code,
    # This tuple is a new version of foo.func_code.co_consts
    # NOTE: Don't get this wrong or you will crash python.
    ( 
       foo.func_code.co_consts[0],
       my_bar.func_code,
       foo.func_code.co_consts[2],
       foo.func_code.co_consts[3],
       foo.func_code.co_consts[4]
    ),
    foo.func_code.co_names,
    foo.func_code.co_varnames,
    foo.func_code.co_filename,
    foo.func_code.co_name,
    foo.func_code.co_firstlineno,
    foo.func_code.co_lnotab,
    foo.func_code.co_freevars,
    foo.func_code.co_cellvars )

# This is the new function we're replacing foo with
# using our new code.
foo = types.FunctionType( foocode , {})

# Now use it
foo()

I'm pretty sure its not going to catch all cases. But it works for the example (for me on an old python 2.5.1 )

Ugly bits that could do with some tidy up are:

  1. The huge argument list being passed to CodeType
  2. The ugly tuple constructed from co_consts overriding only one member. All the info is in co_consts to determine which to replace - so a smarter function could do this. I dug into the internals by hand using print( foo.func_code.co_consts ).

You can find some information about the CodeType and FunctionType by using the interpreter command help( types.CodeType ).

UPDATE: I thought this was too ugly so I built a helper function to make it prettier. With the helper you can write:

# Use our function to get a new version of foo with "bar" replaced by mybar    
foo = monkey_patch_fn( foo, "bar", my_bar )

# Check it works
foo()

Here's the implementation of monkey_patch_fn:

# Returns a copy of original_fn with its internal function
# called name replaced with new_fn.
def monkey_patch_fn( original_fn, name, new_fn ):

  #Little helper function to pick out the correct constant
  def fix_consts(x):
    if x==None: return None
    try:
      if x.co_name == name:
        return new_fn.func_code
    except AttributeError, e:
        pass
    return x

  original_code = original_fn.func_code
  new_consts = tuple( map( fix_consts, original_code.co_consts ) )
  code_type_args = [
     "co_argcount", "co_nlocals", "co_stacksize", "co_flags", "co_code",
     "co_consts", "co_names", "co_varnames", "co_filename", "co_name",
     "co_firstlineno", "co_lnotab", "co_freevars", "co_cellvars" ]

  new_code = types.CodeType(
     *[ ( getattr(original_code,x) if x!="co_consts" else new_consts )
        for x in code_type_args ] )
  return types.FunctionType( new_code, {} )
Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
  • Having both coding approaches to this answer provides twice the insight. – MikeiLL Sep 10 '14 at 17:50
  • 1
    Might be worth noting that the second argument in the FunctionType constructor represents the globals and in this case is set to an empty dictionary. In 2.7, I was having issues with `int` not being defined when I called the modified function; to fix this, I replaced `{}` with `globals=globals()`. – ruyili May 11 '20 at 00:16
  • @ayanokouji A better solution would be to pick out the globals that the original function was using. But I'm not sure how to do that (being 8 years since I looked at this...) If you've any thoughts on how to do that I'd love to update this to properly handle it. – Michael Anderson May 11 '20 at 00:29
  • @MichaelAnderson Good point; I believe it's available through .func_globals. (also yeah sorry for digging up an old thread lol) – ruyili May 11 '20 at 15:24
3

You can pass it in as an optional parameter

def foo(bar=None):
    def _bar():
        # I want to change this
        pass
    if bar is None:
        bar = _bar
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
  • I'm not sure I understand. Doesn't your answer imply to change the given function? Maybe I've not been clear, but I can't change the original `foo`. Btw, I'm going to edit my question a bit. – Paolo Aug 11 '12 at 03:15
  • 4
    I think the OP is looking for some kind of monkeypatch option, as `foo` "comes from a given imported module I can't modify." – PaulMcG Aug 11 '12 at 03:17