60

I need to extend the Networkx python package and add a few methods to the Graph class for my particular need

The way I thought about doing this is simplying deriving a new class say NewGraph, and adding the required methods.

However there are several other functions in networkx which create and return Graph objects (e.g. generate a random graph). I now need to turn these Graph objects into NewGraph objects so that I can use my new methods.

What is the best way of doing this? Or should I be tackling the problem in a completely different manner?

zenna
  • 9,006
  • 12
  • 73
  • 101

9 Answers9

91

If you are just adding behavior, and not depending on additional instance values, you can assign to the object's __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)

Prints:

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

This is as close to a "cast" as you can get in Python, and like casting in C, it is not to be done without giving the matter some thought. I've posted a fairly limited example, but if you can stay within the constraints (just add behavior, no new instance vars), then this might help address your problem.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • 5
    Ok, so what happens when you _do_ need to add variables? – Nicu Stiurca Jun 05 '15 at 15:42
  • 1
    You can add/set instance variables at runtime. Careful though that you don't get confused with instance variable that are added by a CirclePlus __init__ that you forgot to add because this casting method bypasses __init__ I suppose? By the way, since Python's type system can be overridden, this casting method will not always work. – Rian Rizvi Mar 08 '16 at 20:01
  • 2
    If you find that you need to add instance variables too, then I think you are quickly getting beyond the realm of maintainable code - time to rethink your design, probably using some form of containment and/or delegation. – PaulMcG Mar 02 '17 at 20:40
17

Here's how to "magically" replace a class in a module with a custom-made subclass without touching the module. It's only a few extra lines from a normal subclassing procedure, and therefore gives you (almost) all the power and flexibility of subclassing as a bonus. For instance this allows you to add new attributes, if you wish.

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()

So far this is exactly like normal subclassing. Now we need to hook this subclass into the networkx module so that all instantiation of nx.Graph results in a NewGraph object instead. Here's what normally happens when you instantiate an nx.Graph object with nx.Graph()

1. nx.Graph.__new__(nx.Graph) is called
2. If the returned object is a subclass of nx.Graph, 
   __init__ is called on the object
3. The object is returned as the instance

We will replace nx.Graph.__new__ and make it return NewGraph instead. In it, we call the __new__ method of object instead of the __new__ method of NewGraph, because the latter is just another way of calling the method we're replacing, and would therefore result in endless recursion.

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()

In most cases this is all you need to know, but there is one gotcha. Our overriding of the __new__ method only affects nx.Graph, not its subclasses. For example, if you call nx.gn_graph, which returns an instance of nx.DiGraph, it will have none of our fancy extensions. You need to subclass each of the subclasses of nx.Graph that you wish to work with and add your required methods and attributes. Using mix-ins may make it easier to consistently extend the subclasses while obeying the DRY principle.

Though this example may seem straightforward enough, this method of hooking into a module is hard to generalize in a way that covers all the little problems that may crop up. I believe it's easier to just tailor it to the problem at hand. For instance, if the class you're hooking into defines its own custom __new__ method, you need to store it before replacing it, and call this method instead of object.__new__.

Community
  • 1
  • 1
Lauritz V. Thaulow
  • 49,139
  • 12
  • 73
  • 92
  • Can I do this with a built-in? If for example, I want to cast `set` to `SpecialSet` can I change the built-in's `__new__` behavior? – GrantJ Oct 17 '14 at 17:22
  • 1
    @GrantJ That won't work. Most python builtins are implemented in C, and as such are not as malleable as pure python classes. You'll get this error: `TypeError: can't set attributes of built-in/extension type 'set'`. – Lauritz V. Thaulow Oct 17 '14 at 21:28
  • `def __new__(cls):` should also accept more arguments. They are not used in the creation, but will be passed on to the instantiation --> `def __new__(cls, *args, **kwargs):` – ascripter Dec 23 '19 at 16:10
2

I expanded what PaulMcG did and made it a factory pattern.

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))

Result:

{'a': 10, 'a_variable': 'something'}
{'a': 10, 'a_variable': 'something', 'b': 15}
do something A
<class '__main__.B'>
  • 1
    This is an awesome generic way to cast an object of a parent class to a child class. Especially useful for complex objects that need slight alterations. Worked great for me, thanks! – Zenon Anderson Oct 18 '21 at 17:47
1

You could simply create a new NewGraph derived from Graph object and have the __init__ function include something like self.__dict__.update(vars(incoming_graph)) as the first line, before you define your own properties. In this way you basically copy all the properties from the Graph you have onto a new object, derived from Graph, but with your special sauce.

class NewGraph(Graph):
  def __init__(self, incoming_graph):
    self.__dict__.update(vars(incoming_graph))

    # rest of my __init__ code, including properties and such

Usage:

graph = function_that_returns_graph()
new_graph = NewGraph(graph)
cool_result = function_that_takes_new_graph(new_graph)
cjbarth
  • 4,189
  • 6
  • 43
  • 62
  • Mostly in the right direction, but I would also advise the asker to look for specifically what APIs their class provides for this. In the asker's example, a look at the [`Graph` class documentation](https://networkx.org/documentation/stable/reference/classes/graph.html#networkx.Graph) suggests that your subclass could do `self.add_edges_from(original_graph)`. Or as another answer suggests, it looks like their `__init__` already provides copy-construction (as a well-designed library class `__init__` should, in most cases). – mtraceur Oct 28 '22 at 15:11
1

I encountered the same question when contributing to networkx, because I need many new methods for Graph. The answer by @Aric is the simplest solution, but inheritance is not used. Here a native networkx feature is utilise, and it should be more efficient.

There is a section in networkx tutorial, using the graph constructors, showing how to init Graph object from existing objects for a graph, especially, another graph object. This is the example shown there, you can init a new DiGraph object, H, out of an existing Graph object, G:

>>> 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)]

Note the mathematical meaning when converting an existing graph to a directed graph. You can probably realise this feature via some function or constructor, but I see it as an important feature in networkx. Haven't checked their implementation, but I guess it's more efficient.

To preserve this feature in NewGraph class, you should make it able to take an existing object as argument in __init__, for example:

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)

Then whenever you have a Graph object, you can init (NOT turn it directly to) a NewGraph object by:

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

or you can init an empty NewGraph object:

>>> NG_2 = NewGraph()

For the same reason, you can init another Graph object out of NG:

>>> G_2 = nx.Graph(NG)

Most likely, there are many operations after super().__init__() when initiating a NewGraph object, so the answer by @PaulMcG, as he/she mentioned, is not a good idea in such circumstance.

Edward
  • 554
  • 8
  • 15
  • *This* is the right answer. Our first approach in these situations should be to check if the class you are subclassing already supports copy-construction or provides a method to copy data from one instance or another (in this case, it provides both), and use that if available. – mtraceur Oct 28 '22 at 15:35
0

If a function is creating Graph objects, you can't turn them into NewGraph objects.

Another option is for NewGraph is to have a Graph rather than being a Graph. You delegate the Graph methods to the Graph object you have, and you can wrap any Graph object into a new NewGraph object:

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):
        ....
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • 3
    Thanks I read somewhere else that I can just change the __class__ attribute. e.g. MyRandomGraphObject.__class__ = NewGraph. And it does actually work. Bad practice? – zenna Aug 12 '10 at 01:35
0

For your simple case you could also write your subclass __init__ like this and assign the pointers from the Graph data structures to your subclass data.

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)

You could also use copy() or deepcopy() in the assignments but if you are doing that you might as well use

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

to load your graph data.

Aric
  • 24,511
  • 5
  • 78
  • 77
0

The __class__ assignment approach actually alters the variable. If you only want to call a function form the super class you can use super. For example:

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()

is returning

B
A
-3

Have you guys tried [Python] cast base class to derived class

I have tested it, and seems it works. Also I think this method is bit better than below one since below one does not execute init function of derived function.

c.__class__ = CirclePlus