Python3.10 装饰器混淆:如何包装一个类来增强类的 __init__ 例如跟踪函数调用

问题描述 投票:0回答:1

我对 python 装饰器感到困惑。是的,那里有很多有用的资源(我在发布之前已经查阅过它们)

例如

我确信有更好的方法来做我将要描述的事情,那是因为这只是一个简单的玩具示例来理解属性而不是我真正想做的。

假设你有一个类

class foo:
    def __init__(self):
        pass

    def call_me(self, arg):
        return arg

我想扩展

foo
的构造函数以接受两个关键字参数并修改
call_me
方法,例如期望的最终类是:

class foo:
    def __init__(self, keep_track:bool=False, do_print:bool=True):
        self.keep_track = keep_track
        self.memory = []
        ...

    def call_me(self, arg):
        arg = self.old_call_me(arg)
        
        if self.do_print:
            print('hi')

        if self.keep_track:
             self.memory.append(arg)
       
        return arg

    def old_call_me(self, arg):
        ...
        return arg

理想情况下,我想要一个装饰器,这样我就可以包装一堆类假设有一个方法

call_me

@keep_track(do_print=False) # <--- sets default state
class foo:
   ...

@keep_track(keep_track=True) # <--- sets default state
class bar:
   ...


foo_instance = foo(do_print=True) # <-- I can change behavior of instance 

我怎样才能做到这一点?

我尝试在装饰器中定义函数并设置它们

setattr(cls, 'old_call_me', call_me)
def call_me(self, arg):
    # see above
 
setattr(cls, 'call_me', call_me) 

并使用

functools.wrap

在此问题上,我将不胜感激。

python python-decorators
1个回答
2
投票

我可以提供一个非常,非常令人费解,难以阅读且仅用于学习目的的想法......

可以 利用这样一个事实,即如果你尝试访问实例的属性,Python 将首先在实例属性中查找它(假设

self.__dict__
),如果在那里找不到它,它将尝试在实例的类属性中找到它(比方说
self.__class__.__dict__

所以你可以让你的装饰器将默认值写入class本身,然后接受它作为你的

__init__
方法的可选关键字参数(基本上有时只创建
do_print
instance属性)。

from functools import wraps


def my_decorator(*, do_print):
    def my_decorator_inner(klass):
        print(f"Outer: {do_print}")

        @wraps(klass)
        def wrapper(*args, **kwargs):
            print(f"Inner: {klass} {args}, {kwargs}")
            klass.do_print = do_print
            return klass(*args, **kwargs)

        return wrapper

    return my_decorator_inner


@my_decorator(do_print=False)
class Foo:
    def __init__(self, *, do_print=None):
        if do_print is not None:
            self.do_print = do_print

    def call_me(self):
        if self.do_print:
            print('hi')
        else:
            print("nopes, I ain't printing")


f_no_print = Foo()
f_no_print.call_me()

f_print = Foo(do_print = True)
f_print.call_me()

请注意,我添加了一些

print
语句,它们可能有助于传递给每个函数的内容。

但是这个解决方案非常非常复杂和令人困惑,因为它真正在做的只是:

class Foo:
    do_print = False

    def __init__(self, *, do_print=None):
        if do_print is not None:
            self.do_print = do_print

    def call_me(self):
        if self.do_print:
            print('hi')
        else:
            print("nopes, I ain't printing")


f_no_print = Foo()
f_no_print.call_me()

f_print = Foo(do_print=True)
f_print.call_me()

编辑根据以下评论:

在 Python 中,一切皆对象。甚至函数也只是……有点像“”“变量”“”。类也是对象。当您键入

class Foo:
时,您不“只是”创建了一个类定义(好吧……您是,但是……)您正在创建一个名为
Foo
的实例,其类型为
type
。您可以通过元类更改它(这是
__new__
方法通常使用的地方)。然后可以调用
Foo
类型的对象
type
(
inst = Foo()
) 以生成
Foo

类型的实例

所以,首先要做的是。 @Tourelou 在他的评论中建议的 cleaner 子类化方法我想会是这样的:

class Foo:
    def call_me(self, arg):
        return arg


class FooPrinting(Foo):
    def __init__(self, keep_track: bool = False, do_print: bool = True):
        self.keep_track = keep_track
        self.do_print = do_print
        self.memory = []

    def call_me(self, arg):
        arg = super().call_me(arg)
        if self.do_print:
            print(f'hi {arg}')
        if self.keep_track:
            self.memory.append(arg)
        return arg


f_no_print = Foo()
f_no_print.call_me(1)

f_print = FooPrinting(do_print=True)
f_print.call_me(2)

很好,很干净,这可能是您想要使用的...

您在评论中提到

只有一些实例具有关键字 arg 似乎太非 pythonic

实际上,这就是 Pythonic 方式。我知道它一开始看起来可能很混乱,但如果您想保留更改某些实例行为的能力,则必须以某种方式接受构造函数中的参数。具有默认值的 kwargs 是一种很好的方式。是的,是的:您真的 必须 接受构造函数中的参数,因为您可以在代码中的任何位置向任何实例动态添加属性……但您 应该。事实上你可以做某事并不意味着你应该这样做(这最后一行也适用于下面的内容):

现在,由于您似乎只想对方法进行猴子修补,因此可以选择通过另一种方法(在下面的示例中为

.call_me
)动态交换类的
new_call_me
方法,您“保留在freezer" 并且你换了装饰器:

from functools import wraps


def new_call_me(self, arg):
    arg = self.old_call_me(arg)
    if getattr(self, 'do_print', False):
        print(f'swapped hi {arg}')
    if getattr(self, 'keep_track', False):
        self.memory.append(arg)
    return arg


def my_decorator(*, do_print):
    def my_decorator_inner(klass):
        print(f"Outer: {do_print}")

        @wraps(klass)
        def wrapper(*args, **kwargs):
            print(f"Inner: {klass} {args}, {kwargs}")
            klass.do_print = do_print

            if (not (hasattr(klass, 'old_call_me'))  # <- If already swapped, skip
                    and hasattr(klass, 'call_me') and callable(klass.call_me)):
                print(f"Swapping method in class {klass.__name__}")
                klass.old_call_me = klass.call_me
                klass.call_me = new_call_me
            return klass(*args, **kwargs)

        return wrapper

    return my_decorator_inner


@my_decorator(do_print=True)
class Foo:
    def __init__(self, *, do_print=None):
        self.do_print = do_print

    def call_me(self, arg):
        return arg


f_no_print = Foo()
f_no_print.call_me(1)

f_print = Foo(do_print=True)
f_print.call_me(2)

⚠️ 但是: 如果我在我公司的代码库中看到这个,我会大喊大叫。如果我对此事有任何发言权,我会设法解雇这种反常行为的作者。好的,很好,很好!...也许不是那么多,因为我内心是个软蛋😂,但肯定会和那个人交谈(CON-VER-SA-TION!)。

© www.soinside.com 2019 - 2024. All rights reserved.