jazcap53
jazcap53

Reputation: 401

python 3: how do I create two different classes that operate on the same data?

I have a class that looks something like the following:

# Class violates the Single Responsibility Principle
class Baz:
    data = [42]
    def do_foo_to_data(self):
        # call a dozen functions that do complicated stuff to data

    def do_bar_to_data(self):
        # call other functions that do different stuff to data

I want to break it into two separate classes because it violates the SRP. The functions called by do_foo_to_data() are completely distinct from those called by do_bar_to_data(). Yet they must operate on the same data.

I've come up with a bunch of solutions, but they're all ugly. Is there a way to do this cleanly, preferably in Python 3 (though 2.7 is OK too)?

The best of my "solutions" is below:

# I find this hard to read and understand
class Baz:
    data = [42]

    def create_foo(self):
        return Baz.Foo()

    def create_bar(self):
        return Baz.Bar()


    class Foo:
        def do_foo_to_data(self):
            # call foo functions


    class Bar:
        def do_bar_to_data(self):
            # call bar functions

Note: It's not essential to me that the data member be a class member. I only expect to create one instance of Baz; but I didn't want to ask two questions in one post and start a discussion about singletons.

Upvotes: 0

Views: 173

Answers (3)

das-g
das-g

Reputation: 9994

You can pull out the distinct groups of functionality to separate mix-in classes:

class Foo:
    """Mixin class.

    Requires self.data (must be provided by classes extending this class).
    """
    def do_foo_to_data(self):
        # call a dozen functions that do complicated stuff to data

class Bar:
    """Mixin class.

    Requires self.data (must be provided by classes extending this class).
    """
    def do_bar_to_data(self):
        # call other functions that do different stuff to data

class Baz(Foo, Baz):
    data = [42]

This relies on Python's duck-typing behavior. You should only apply the Foo and Bar mix-ins to classes that actually provide self.data, like the Baz class here does.

This might be suitable where certain classes are by convention required to provide certain attributes anyway, such as customized view classes in Django. However, when such conventions aren't already in place, you might not want to introduce new ones. It's too easy to miss the documentation and then have NameErrors at runtime. So let's make the dependency explicit, rather than only documenting it. How? With a mix-in for the mix-ins!

class Data:
    """Mixin class"""
    data = [42]

class Foo(Data):
    """Mixin class"""
    def do_foo_to_data(self):
        # call a dozen functions that do complicated stuff to data

class Bar(Data):
    """Mixin class"""
    def do_bar_to_data(self):
        # call other functions that do different stuff to data

class Baz(Foo, Baz):
    pass

Whether this is appropriate for your use-case is difficult to say at this level of abstraction. As RayLuo's answer shows, you might not need classes at all. Instead, you could put the different groups of functions into different modules or packages, to organize them.

Upvotes: 1

RayLuo
RayLuo

Reputation: 19210

Why do you even need a class for that? All you want is two separated functions which do some job on some data.

data = [42]

def foo(data):
    data.append('sample operation foo')

def bar(data):
    data.append('sample operation bar')

Problem solved.

Upvotes: 4

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476574

This is not an elegant solution. You better pass a reference to the object you want them to operate on. So something like:

class Foo:

    def __init__(self,data):
        self.data = data

    def do_foo_to_data(self):
        #...
        self.data[0] = 14
        pass

class Bar:

    def __init__(self,data):
        self.data = data

    def do_bar_to_data(self):
        #...
        self.data.append(15)
        pass

(I added sample manipulations like self.data[0] = 14 and self.data.append(15))

And now you construct the data. For instance:

data = [42]

Next you construct a Foo and a Bar and pass a reference to data like:

foo = Foo(data)
bar = Bar(data)

__init__ is what most programming languages call the constructor and as you have seen in the first fragment, it requires an additional parameter data (in this case it is a reference to our constructed data).

and then you can for instance call:

foo.do_foo_to_data()

which will set data to [14] and

bar.do_bar_to_data()

which will result in data being equal to [14,15].

Mind that you cannot state self.data = ['a','new','list'] or something equivalent in do_foo_to_data or do_bar_to_data because this would change the reference to a new object. Instead you could for instance .clear() the list, and append new elements to it like:

def do_foo_to_data(self): #alternative version
    #...
    self.data.clear()
    self.data.append('a')
    self.data.append('new')
    self.data.append('list')

Finally to answer your remark:

preferably in Python 3 (though 2.7 is OK too)?

The technique demonstrated is almost universal (meaning it is available in nearly every programming language). So this will work in both and .

Upvotes: 4

Related Questions