【问题标题】:Cast base class to derived class python (or more pythonic way of extending classes)将基类转换为派生类python(或扩展类的更多pythonic方式)
【发布时间】:2011-03-28 16:51:59
【问题描述】:

我需要扩展 Networkx python 包并向Graph 类添加一些方法以满足我的特殊需要

我想这样做的方法是简单地派生一个新类,比如NewGraph,并添加所需的方法。

不过,networkx 中还有其他几个函数可以创建和返回 Graph 对象(例如,生成随机图)。我现在需要将这些Graph 对象转换为NewGraph 对象,以便我可以使用我的新方法。

这样做的最佳方法是什么?还是我应该以完全不同的方式解决问题?

【问题讨论】:

    标签: python inheritance derived-class base-class


    【解决方案1】:

    如果您只是添加行为,而不依赖于其他实例值,则可以分配给对象的__class__

    from math import pi
    
    class Circle(object):
        def __init__(self, radius):
            self.radius = radius
    
        def area(self):
            return pi * self.radius**2
    
    class CirclePlus(Circle):
        def diameter(self):
            return self.radius*2
    
        def circumference(self):
            return self.radius*2*pi
    
    c = Circle(10)
    print c.radius
    print c.area()
    print repr(c)
    
    c.__class__ = CirclePlus
    print c.diameter()
    print c.circumference()
    print repr(c)
    

    打印:

    10
    314.159265359
    <__main__.Circle object at 0x00A0E270>
    20
    62.8318530718
    <__main__.CirclePlus object at 0x00A0E270>
    

    这与 Python 中的“强制转换”非常接近,并且与 C 中的强制转换一样,如果不考虑问题,就不能这样做。我发布了一个相当有限的示例,但如果您可以保持在约束范围内(只需添加行为,没有新的实例变量),那么这可能有助于解决您的问题。

    【讨论】:

    • 好的,那么当您确实需要添加变量时会发生什么?
    • 您可以在运行时添加/设置实例变量。请注意,您不会对由您忘记添加的 CirclePlus init 添加的实例变量感到困惑,因为这种转换方法绕过了 init 我想?顺便说一句,由于 Python 的类型系统可以被覆盖,所以这种转换方法并不总是有效。
    • 如果你发现你也需要添加实例变量,那么我认为你很快就会超越可维护代码的领域——是时候重新考虑你的设计了,可能使用某种形式的包含和/或委托.
    【解决方案2】:

    以下是如何在不触及模块的情况下“神奇地”用定制的子类替换模块中的类。它只是正常子类化过程中的几行额外代码,因此(几乎)为您提供了子类化的所有功能和灵活性作为奖励。例如,如果您愿意,这允许您添加新属性。

    import networkx as nx
    
    class NewGraph(nx.Graph):
        def __getattribute__(self, attr):
            "This is just to show off, not needed"
            print "getattribute %s" % (attr,)
            return nx.Graph.__getattribute__(self, attr)
    
        def __setattr__(self, attr, value):
            "More showing off."
            print "    setattr %s = %r" % (attr, value)
            return nx.Graph.__setattr__(self, attr, value)
    
        def plot(self):
            "A convenience method"
            import matplotlib.pyplot as plt
            nx.draw(self)
            plt.show()
    

    到目前为止,这与正常的子类化完全一样。现在我们需要将这个子类挂接到networkx 模块中,这样nx.Graph 的所有实例化都会生成NewGraph 对象。以下是使用 nx.Graph() 实例化 nx.Graph 对象时通常会发生的情况

    1. nx.Graph.__new__(nx.Graph) 被调用 2.如果返回的对象是nx.Graph的子类, __init__ 在对象上被调用 3.对象作为实例返回

    我们将替换 nx.Graph.__new__ 并使其返回 NewGraph。其中,我们调用object__new__ 方法而不是NewGraph__new__ 方法,因为后者只是调用我们要替换的方法的另一种方式,因此会导致无限递归。

    def __new__(cls):
        if cls == nx.Graph:
            return object.__new__(NewGraph)
        return object.__new__(cls)
    
    # We substitute the __new__ method of the nx.Graph class
    # with our own.     
    nx.Graph.__new__ = staticmethod(__new__)
    
    # Test if it works
    graph = nx.generators.random_graphs.fast_gnp_random_graph(7, 0.6)
    graph.plot()
    

    在大多数情况下,这就是您需要知道的全部内容,但有一个问题。我们对__new__ 方法的覆盖只影响nx.Graph,而不影响它的子类。例如,如果您调用nx.gn_graph,它返回一个nx.DiGraph 的实例,它将没有我们花哨的扩展。您需要对您希望使用的nx.Graph 的每个子类进行子类化,并添加所需的方法和属性。使用mix-ins 可以更容易地一致地扩展子类,同时遵守DRY 原则。

    虽然这个例子看起来很简单,但这种连接到模块的方法很难概括为涵盖所有可能出现的小问题。我相信根据手头的问题对其进行调整会更容易。例如,如果您要挂钩的类定义了自己的自定义 __new__ 方法,则需要在替换它之前将其存储,并调用此方法而不是 object.__new__

    【讨论】:

    • 我可以使用内置的吗?例如,如果我想将set 转换为SpecialSet,我可以更改内置的__new__ 行为吗?
    • @GrantJ 那行不通。大多数 python 内置函数都是用 C 实现的,因此不像纯 python 类那样具有延展性。你会得到这个错误:TypeError: can't set attributes of built-in/extension type 'set'.
    • def __new__(cls): 也应该接受更多参数。它们在创建时不使用,但会传递给实例化 --> def __new__(cls, *args, **kwargs):
    【解决方案3】:

    我扩展了 PaulMcG 所做的工作,并使其成为工厂模式。

    class A:
     def __init__(self, variable):
        self.a = 10
        self.a_variable = variable
    
     def do_something(self):
        print("do something A")
    
    
    class B(A):
    
     def __init__(self, variable=None):
        super().__init__(variable)
        self.b = 15
    
     @classmethod
     def from_A(cls, a: A):
        # Create new b_obj
        b_obj = cls()
        # Copy all values of A to B
        # It does not have any problem since they have common template
        for key, value in a.__dict__.items():
            b_obj.__dict__[key] = value
        return b_obj
    
    if __name__ == "__main__":
     a = A(variable="something")
     b = B.from_A(a=a)
     print(a.__dict__)
     print(b.__dict__)
     b.do_something()
     print(type(b))
    

    结果:

    {'a': 10, 'a_variable': 'something'}
    {'a': 10, 'a_variable': 'something', 'b': 15}
    do something A
    <class '__main__.B'>
    

    【讨论】:

    • 这是将父类的对象转换为子类的一种很棒的通用方法。特别适用于需要稍作改动的复杂对象。非常适合我,谢谢!
    【解决方案4】:

    如果函数正在创建 Graph 对象,则不能将它们转换为 NewGraph 对象。

    NewGraph 的另一个选择是拥有一个 Graph 而不是成为一个 Graph。您将 Graph 方法委托给您拥有的 Graph 对象,并且您可以将任何 Graph 对象包装到新的 NewGraph 对象中:

    class NewGraph:
        def __init__(self, graph):
            self.graph = graph
    
        def some_graph_method(self, *args, **kwargs):
            return self.graph.some_graph_method(*args, **kwargs)
        #.. do this for the other Graph methods you need
    
        def my_newgraph_method(self):
            ....
    

    【讨论】:

    • 谢谢我在其他地方读到我可以更改 class 属性。例如MyRandomGraphObject.__class__ = NewGraph。它确实有效。不好的做法?
    【解决方案5】:

    对于您的简单情况,您还可以像这样编写子类 __init__ 并将 Graph 数据结构中的指针分配给您的子类数据。

    from networkx import Graph
    
    class MyGraph(Graph):
        def __init__(self, graph=None, **attr):
            if graph is not None:
                self.graph = graph.graph   # graph attributes
                self.node = graph.node   # node attributes
                self.adj = graph.adj     # adjacency dict
            else:
                self.graph = {}   # empty graph attr dict
                self.node = {}    # empty node attr dict 
                self.adj = {}     # empty adjacency dict
    
            self.edge = self.adj # alias 
            self.graph.update(attr) # update any command line attributes
    
    
    if __name__=='__main__':
        import networkx as nx
        R=nx.gnp_random_graph(10,0.4)
        G=MyGraph(R)
    

    您也可以在作业中使用 copy() 或 deepcopy(),但如果您这样做,您不妨使用

    G=MyGraph()
    G.add_nodes_from(R)
    G.add_edges_from(R.edges())
    

    加载您的图表数据。

    【讨论】:

    • 这对我有用。但是如何使用双下划线方法呢?
    【解决方案6】:

    您可以简单地创建一个从Graph 对象派生的新NewGraph,并让__init__ 函数在第一行包含self.__dict__.update(vars(incoming_graph)) 之类的内容,然后再定义自己的属性。通过这种方式,您基本上可以将 Graph 中的所有属性复制到一个新对象上,该对象源自 Graph,但带有您的特殊调味料。

    class NewGraph(Graph):
      def __init__(self, incoming_graph):
        self.__dict__.update(vars(incoming_graph))
    
        # rest of my __init__ code, including properties and such
    

    用法:

    graph = function_that_returns_graph()
    new_graph = NewGraph(graph)
    cool_result = function_that_takes_new_graph(new_graph)
    

    【讨论】:

      【解决方案7】:

      我在为networkx 投稿时遇到了同样的问题,因为我需要很多Graph 的新方法。 answer by @Aric 是最简单的解决方案,但不使用继承。这里使用了原生的networkx 功能,应该会更高效。

      a section in networkx tutorial, using the graph constructors,展示了如何从一个图的现有对象初始化Graph 对象,尤其是另一个图对象。这是那里显示的示例,您可以从现有的Graph 对象G 中初始化一个新的DiGraph 对象H

      >>> G = Graph()
      >>> G.add_edge(1, 2)
      >>> H = nx.DiGraph(G)   # create a DiGraph using the connections from G
      >>> list(H.edges())
      [(1, 2), (2, 1)]
      

      将现有图转换为有向图时,请注意数学含义。您可能可以通过一些函数或构造函数来实现此功能,但我将其视为networkx 中的一个重要功能。没有检查他们的实现,但我想它更有效。

      要在NewGraph 类中保留此功能,您应该使其能够将现有对象作为__init__ 中的参数,例如:

      from typing import Optional
      import networkx as nx
      
      
      class NewGraph(nx.Graph):
      
          def __init__(self, g: Optional[nx.Graph] = None):
              """Init an empty directed graph or from an existing graph.
      
              Args:
                  g: an existing graph.
              """
              if not g:
                  super().__init__()
              else:
                  super().__init__(g)
      

      然后,只要您有一个Graph 对象,您就可以通过以下方式初始化(不要直接将其转为)NewGraph 对象:

      >>> G = nx.some_function()
      ...
      >>> NG = NewGraph(G)
      

      或者你可以初始化一个空的NewGraph对象:

      >>> NG_2 = NewGraph()
      

      出于同样的原因,您可以从NG 中初始化另一个Graph 对象:

      >>> G_2 = nx.Graph(NG)
      

      很可能,在启动NewGraph 对象时,super().__init__() 之后有很多操作,所以他/她提到的answer by @PaulMcG 在这种情况下不是一个好主意。

      【讨论】:

        【解决方案8】:

        __class__ 赋值方法实际上改变了变量。如果你只想从超类调用一个函数,你可以使用super。例如:

        class A:
            def __init__(self):
                pass
            def f(self):
                print("A")
        
        class B(A):
            def __init__(self):
                super().__init__()
            def f(self):
                print("B")
        
        b = B()
        b.f()
        super(type(b), b).f()
        

        回来了

        B
        A
        

        【讨论】:

          【解决方案9】:

          你们试过了吗 [Python] cast base class to derived class

          我已经测试过了,它似乎有效。另外我认为这种方法比下面的方法好一点,因为下面的方法不执行派生函数的 init 函数。

          c.__class__ = CirclePlus
          

          【讨论】:

            猜你喜欢
            • 2013-04-27
            • 1970-01-01
            • 2017-07-18
            • 2017-01-08
            • 2013-11-17
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多