Python:如何重写类的方法,同时保留装饰器并调用原始方法?

1 投票
4 回答
2543 浏览
提问于 2025-04-18 17:22

我该如何在这种情况下重写一个类的方法呢?不过,我不能修改solid.py和run_me.py这两个文件。同时,我还需要保持装饰器的功能,并能够调用原来的方法。

# - solid.py - (no control)
import http

class Solid(object):
    _cp_path = '/pos'

    @http.jsonrequest
    def break_apart(self):
        return "to pieces!"

;

# - me.py -
import solid

def break_apart(self):
    return "to sand! and " + super(solid.Solid, self).break_apart()

solid.Solid.break_apart = break_apart

;

# - run_me.py - (no control)
import me    # yes, me first
import solid

pebble = solid.Solid()
pebble.break_apart() # "to sand! and to pieces!"

编辑

感谢大家的帮助,抱歉之前信息不全。我忘了提到solid.py里有装饰器。使用monkeypatch的方法效果很好,但有几个问题:

  • 我失去了原来的装饰器
  • 我无法调用原来的方法

    出现了错误:AttributeError: 'super'对象没有'break_apart'这个属性

4 个回答

0

在StackOverflow这个知识的宝库里,我找到了一种方法,这对我有效。我知道这个方法有点复杂,但我还没有找到更简单的。这个代码适用于Python3。

import functools
def log( fn ):
    @functools.wraps( fn )
    def wrapper( *args, **kwargs ):
        print( "log", fn.__name__, args[ 1: ] )
        return fn( *args, **kwargs )
    return wrapper

def prepare_class( clazz ):
    @classmethod
    def on_class_patcher( cls, func_name, context ):
        def patch_by_name( new_func) :
            original_func = getattr( cls, func_name )
            def patched_func( self, *args, **kwargs ):
                return new_func( self, original_func, context, *args, **kwargs )
            setattr( cls, func_name, patched_func )
        return patch_by_name

    setattr( clazz, "patch", on_class_patcher )

# --- Use like this ---
class Demo:
    @log
    def m1( self, x, y ):
        print( "Demo.m1" )
        print( x, y )

    def m2( self ):
        print( "Demo.m2" )

class Foo:
    def m3( self ):
        print( "Foo.m3" )

prepare_class( Demo )
foo = Foo()

@Demo.patch( "m1", context = { "foo": foo } )
def patched_m1( self, original_func, context, *args, **kwargs ):
    print( "Demo.m1_patched" )
    self.m2()
    context[ "foo" ].m3()
    x = args[ 0 ] * 2
    y = args[ 1 ] * 2
    return original_func( self, x, y )

demo = Demo()
demo.m1( 1, 2 )

结果:

Demo.m1_patched
Demo.m2
Foo.m3
log m1 (2, 4)
Demo.m1
2 4
0

我觉得有两种方法可以用来覆盖一个你无法编辑源代码的类中的方法。

第一种方法是创建一个子类,并在这个子类中重写那个方法。这种方法比较简单,但它只会影响你自己用子类的构造函数创建的对象,而不会影响原来的类:

me.py:

import http

import solid

class MySolid(solid.Solid):
    @http.jsonjequest
    def break_apart(self):
        return "to sand! and " + super(solid.Solid, self).break_apart()

我其实不知道http.jsonrequest这个装饰器具体做什么,所以如果它以某种方式修改了调用方式,你可能需要调整一下调用原始值的方式。

第二种方法是对现有的类进行“猴子补丁”。这意味着你用自己的版本替换掉类中的方法实现。如果原始类的实例是由其他你无法控制的代码创建的,这种方法就很有用。需要注意的是,如果你还需要访问原始方法的实现,你得自己保存一个引用(super并不能处理这种情况)。

me.py:

import http

import solid

_orig_break_apart = solid.Solid.break_apart

@http.jsonrequest
def break_apart(self):
    return "to sand! and " + _orig_break_apart(self)

solid.Solid.break_apart = break_apart

在这里,如果装饰器改变了调用方式,你可能也需要调整一下如何调用原始方法。

装饰后的版本和未装饰的版本可能差别很大(比如,它可能彻底改变了调用方式或返回值)。如果是这样,想要“逆转”这个过程以获取原始方法的实现可能会比较困难,这样你就无法在新版本中调用它。在这种情况下,你可能需要把现有实现的代码复制到你的重写版本中(使用上面提到的任一覆盖方法)。在示例代码中,你可以直接返回"to sand! and to pieces!",而不去调用原始方法。在实际代码中,这可能会复杂一些,但基本思路是一样的。

1

首先,你到底有什么样的类方法呢?

看起来像是一个 staticmethod,但是你没有使用 staticmethod 装饰器。

实例方法的写法是:

class C:
    def instance_method(self, *args, **kwargs):
        return self.foo

类方法的写法是:

    @classmethod
    def class_method(cls, *args, **kwargs):
        return cls.foo

静态方法的写法是:

    @staticmethod
    def static_method(*args, **kwargs):
        return foo

如果你不能修改 run_me.py 文件,那么你就不能通过继承来改变这段代码。

不过,你可以简单地用一个兼容的 break_apart 实现来 monkeypatch 这个 Solid 类,像这样:

import solid

def break_apart(self):
    return "whatever you want"

solid.Solid.break_apart = break_apart

当然,由于你的代码是先被导入的,你其实可以直接替换整个类。

所以就像你现在做的那样定义 Concrete,然后在 solid 模块里对整个类进行猴子补丁:

import solid

class Concrete:
    ...

solid.Solid = Concrete
1

因为Python的函数是“第一类”的,也就是说它们可以像其他数据一样被使用,所以你可以把你的me.py写成这样:

import solid

# replacement method
def break_apart(self):
    return "to sand!"

# override Solid.break_apart with the new method
solid.Solid.break_apart = break_apart

另外,由于你调用了pebble.break_apart(),这就意味着solid.py应该是这样的:

class Solid(object):
    def break_apart(self):
        return "to pieces!"

注意到在break_apart函数里加了self这个参数。当你调用pebble.break_apart()的时候,这个self就是指pebble这个实例。

撰写回答