0

Let's say I have some class, and within instances of that class I want to be able to output certain data to various file types e.g. CSV, SQL, PDF, etc.

The simplest way is just a series of if statements, but that is generally a closed system. Anybody who wants to extend the code has to have control of the class and the ability to modify the existing code.

Here is a really contrived example just to give something a little more concrete:

class Animal:
    def __init__(self,species,eats):
        self.species = species
        self.eats    = eats
        self.data_outputs {'sql':(server,table), 'csv':path}    

    def is_carnivorous(self):
        self.eats == 'meat'

    def is_hippie(self):
        self.eats == 'wheatgrass'

    def write(self,output_type,data):
        output_loc = self.data_outputs[output_type]
        if output_type   == 'sql':
            cursor = connection.cursor()
            cursor.execute(some_insert_statement,data)
        elif output_type  == 'csv':
            with open(path,"r") as out:
                out.write(data)

If the logic for write is very involved, maybe delegating to a class to handle the logic could make the method a little bit cleaner:

class Animal:
    def __init__(self,species,eats):
        self.species = species
        self.eats    = eats
        self.data_outputs {'sql':(server,table), 'csv':path}    

    def is_carnivorous(self):
        self.eats == 'meat'

    def is_hippie(self):
        self.eats == 'wheatgrass'

    def write(self,output_type,data):
            output_loc = self.data_outputs[output_type]
            if output_type   == 'sql':
                SQLWriter.write(*output_loc,self.data)
            elif output_type  == 'csv':
                CSVWriter.write(output_loc,data)


class SQLWriter:
    def write(self,server,table,data):
        cursor = connection.cursor()
        cursor.execute(some_insert_statement,data)


class CSVWriter:
    def write(self,path,data):
        with open(path,"r") as out:
            out.write(data)    

Either way, both systems are closed, since modifying the write behavior requires subclassing the entire Animal class just to override write. It's arguably even worse in the class delegation example because now you've coupled class creation with the write method so now every time you want to make a new output type you have to both modify the write_data method ,and make the appropriate class.

Is the pythonic solution to something like this to just use SQLWriter and CSVWriter as mixins?:

class Animal(CSVWriter):
    def __init__(self,species,eats):
        self.species = species
        self.eats    = eats

    def is_carnivorous(self):
        self.eats == 'meat'

    def is_hippie(self):
        self.eats == 'wheatgrass'

This keeps the system open, in that anybody who wants to make a new output type simply needs to write a new mixin and inherit from it, and no existing code needs to be touched to do this. However - the users of Animal need to be aware that they must inherit from a mixin to get the write capability, which doesn't seem ideal.

If the write method was dependent on the actual type of class containing the data (Animal in this example), this would be straightforward in that i'd just have a write method in that class which any subclassers can override. But in this example I want the write method to be polymorphic on specific data within the class, not on the class itself.

What is the standard Pythonic (or more generally Object Oriented) solution to this?


EDIT: If you're going to downvote - at least explain what your problem with the question is so I can fix it accordingly.

  • I can't answer in terms of the "python way" or the tools that knowledgeable python practitioners work. I can say that your second option is definitely more modular, and separates the concern of how to write from the concern of how to be an animal. Let your CSVWriter or your SQLWriter handle the details that it needs to, and your Animal be ignorant of what writer you are using. Just supply what you need, and as long as they have a common interface, you don't need to worry about the output type. – Berin Loritsch Jul 30 '18 at 20:11
  • @BerinLoritsch appreciate the response - by second version do you mean the mixin example (bottom-most), or the class delegation example? There are actually three versions 1.)if statements with all logic embedded 2.) if statement with class delegation 3.) mixins – Solaxun Jul 30 '18 at 20:19
  • 1
    Have a look at visitor pattern. I've used visitors for collecting data and generating outputs in different formats. – Nick Alexeev Jul 30 '18 at 21:05
  • 1
    "I want the write method to be polymorphic on specific data within the class, not on the class itself." That just screams that you have a class hiding within your class. Pull the data and the method out into their own object and delegate to it. – candied_orange Jul 31 '18 at 1:03
  • 1
    @AnthonyJClink Interesting thought - so you saying to create the relevant writer classes and then simply pass whichever one you want in during instance creation? Seems similar to the mixin approach. – Solaxun Jul 31 '18 at 4:23
3

In both your examples, the Animal class is tightly coupled to the writers, since Animal directly uses SQLWriter and CSVWriter. If you want to add a new output format, you have to touch the Animal class. This may not seem so bad at first, but once you get more writable classes, adding new formats becomes a huge workload because all those classes need to be changed. In terms of complexity, adding a new format is O(n) where n is the number of writable classes.

A better solution is to pass the writer as argument to the write function. Then you can change the output format just by passing a different writer and the writable classes stay untouched. The following example should give you an idea:

class Animal(object):
    def __init__(self, species, eats):
        self.species = species
        self.eats = eats

    def store(self, writer):
        writer.write(self.species)
        writer.write(self.eats)


class SQLWriter(object):
    def __init__(self, server, table):
        self.server = server
        self.table = table

    def write(self, data):
        print("Storing", data, "in SQL", self.server, self.table)


class CSVWriter(object):
    def __init__(self, path):
        self.path = path

    def write(self, data):
        print("Storing", data, "in CSV file", self.path)


def main():
    my_animal = Animal("some_species", "some_food")

    csv_writer = CSVWriter("some_file.csv")
    my_animal.store(csv_writer)

    sql_writer = SQLWriter("some_server", "some_table")
    my_animal.store(sql_writer)


if __name__ == "__main__":
    main()
  • This is the option I would go with. It's essentially a simple form of the GoF Builder pattern (not to be confused with Builder as described by Josh Bloch.) This approach starts to shine when you have multiple complex representations. The class describes itself to the Builder and the Builder builds a representation based on that description. – JimmyJames Jul 31 '18 at 13:47
  • 1
    This option definitely fits best - I was vastly over-complicating things. I did look into the visitor pattern recommend above, but it seemed heavy handed for what I wanted to do (don't need double dispatch, just single on the first argument). – Solaxun Jul 31 '18 at 16:23
0

The system wont allow comment as a newby to the site!

This sounds a lot like method/function overloading.

I come from C++ background, which supports comprehensive function and method overloading. I know Python is also supposed to support it - but I can't be sure to what extent.

The idea is in your Animal class you can define as many versions of Writer() as you like, all with the same name "writer" but signatures different on the type and number of parameters.

Used in its simplest form, if you know your data is SQL then invoke Writer("SQL",connection etc) and if you know your data is CSV invoke Writer("CSV", filepath etc).

With a bit of thought this could be made automatic and the suggestion of Visitor pattern or more likely "double dispatch" would get you in the right direction.

Overloading of function names may be what you mean when you ask for "polymorphism on data". (Or not if I misunderstood what you are asking :-) )

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.