使用元类替换类定义?

3

Python 3.6

我正在尝试修改第三方库的行为。

我不想直接更改源代码。

考虑下面的代码:

class UselessObject(object):
    pass


class PretendClassDef(object):
    """
    A class to highlight my problem
    """

    def do_something(self):

        # Allot of code here

        result = UselessObject()

        return result

我想要用自己的类替换UselessObject

我想知道在我的模块中使用元类拦截创建UselessObject是否是一个有效的想法?

编辑

这个由Ashwini Chaudhary发布的回答可能对其他人有用。还有下面的答案。

另外,我发现在Python 3中,'module'级别的__metaclass__不起作用。所以我的最初问题“它是否是一个有效的想法”是错误的。


2
为什么不直接使用 import that_library; that_library.UselessObject = MyOwnObject 呢? - Aran-Fey
类似于这个可能会有所帮助。 - Ashwini Chaudhary
太棒了!我不知道你可以这样做!@ Ashwini Chaudhary- 是的,答案和那篇帖子更好地描述了我的问题。 - James Schinner
@Rawing,是的,那是个好主意! - James Schinner
给出的答案是正确的,但我们应该注意这与“元类”无关 - 这被称为“猴子补丁”。 - jsbueno
3个回答

3

FWIW,这里有一些代码,说明了Rawing的想法。

class UselessObject(object):
    def __repr__(self):
        return "I'm useless"

class PretendClassDef(object):
    def do_something(self):
        return UselessObject()

# -------

class CoolObject(object):
    def __repr__(self):
        return "I'm cool"

UselessObject = CoolObject

p = PretendClassDef()
print(p.do_something())

输出

I'm cool

我们甚至可以使用这种技术,如果 CoolObject 需要继承 UselessObject。 如果我们将 CoolObject 的定义更改为:
class CoolObject(UselessObject):
    def __repr__(self):
        s = super().__repr__()
        return "I'm cool, but my parent says " + s

我们得到了以下输出:
I'm cool, but my parent says I'm useless

这个方法可行是因为在执行 CoolObject 类定义时,UselessObject 名称还保留着其旧的定义。

2
你真的需要添加 import otherother.UselessObject = CoolObject 才能使它在 OP 的情况下工作。 - Markus Meskanen
@MarkusMeskanen 是的,你可以从Rawing的评论中看到这一点,但这只是一个简单的MCVE,可以放入一个文件中。我不想麻烦地创建一个需要读者创建两个单独文件才能测试几行代码的示例。 - PM 2Ring
我理解你的想法,但这是SO(Stack Overflow),你应该直接回答OP的问题,在他的情况下,他首先没有定义一个类,而是使用了别人包中的一个类。 - Markus Meskanen
如果CoolObject需要继承UselessObject,这个答案是否足够? - Brōtsyorfuzthrāx
@Shule。当然!我刚刚添加了一些代码来说明。 - PM 2Ring

2

这不是元类的工作。

相反,Python 允许您通过一种称为“Monkeypatching”的技术在运行时替换对象。在这种情况下,在调用 thirdyparty.PretendClassDef.do_something 之前,您将把 thirdyparty.UselessObject 替换为 your.CoolObject。

要做到这一点,只需进行简单的赋值操作。因此,假设您在问题中提供的示例片段是 trirdyparty 模块,则您的代码如下:

import thirdyparty

class CoolObject:
    # Your class definition here

thirdyparty.UselesObject = Coolobject

需要注意的事项:您必须更改指向UselessObject所指的对象,以符合目标模块中的使用方式。

例如,如果您的PretendedClassDef和UselessObject在不同的模块中定义,那么如果使用from .useless import UselessObject导入UselessObject(在这种情况下上面的示例就可以),而使用import .useless并将其后作为useless.UselessObject使用,则必须对useless模块进行修补。

此外,Python的unittest.mock具有一个很好的patch可调用项,可以正确执行monkeypatching,如果您希望修改在有限范围内有效的修改,例如在自己的函数或with块内。如果您不想在程序的其他部分更改第三方模块的行为,则可能会发生这种情况。

至于元类,只有当您需要更改要替换的类的元类时才有用,并且只有当您想要在继承UselessObject的类中插入行为时才有用。在这种情况下,它将用于创建本地CoolObject,您仍将执行上述操作,但需要注意,在任何派生类的类体运行之前执行monkeypatching,当进行任何来自第三方库的导入时要非常小心(如果这些子类在同一文件中定义,则会很棘手)。


谢谢!我从来没有完全理解过什么是“猴子补丁”。我得去看看unittest.mock - James Schinner

0

这只是在 PM 2Ring 和 jsbueno 的回答基础上增加了更多的上下文:

如果你正在创建一个供他人使用的第三方库(而不是你使用第三方库),并且如果你需要让 CoolObject 继承 UselessObject 以避免重复,以下内容可能有助于避免在某些情况下出现无限递归错误:

module1.py

class Parent:
    def __init__(self):
        print("I'm the parent.")

class Actor:
    def __init__(self, parent_class=None):
        if parent_class!=None: #This is in case you don't want it to actually literally be useless 100% of the time.
            global Parent
            Parent=parent_class
        Parent()

module2.py

from module1 import *

class Child(Parent):
    def __init__(self):
        print("I'm the child.")

class LeadActor(Actor): #There's not necessarily a need to subclass Actor, but in the situation I'm thinking, it seems it would be a common thing.
    def __init__(self):
        Actor.__init__(self, parent_class=Child)

a=Actor(parent_class=Child) #prints "I'm the child." instead of "I'm the parent."
l=LeadActor() #prints "I'm the child." instead of "I'm the parent."

请注意用户不要为 Actor 的不同子类设置不同的 parent_class 值。也就是说,如果您制作多种演员类型,则只需设置一次 parent_class,除非您希望其对所有演员都进行更改。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接