1

Goal

I am doing a data processing and visualization program using Python with PyQt, Numpy and Matplotlib. The data is imported from csv or binary files (with np.genfromtxt and np.fromfile functions) as member variables of the main window in Numpy arrays. I want to be able to access this data from a parent (or a sub-parent) widget which draws curves in a Matplotlib canvas.

My Ideas

  • I considered to use QApplication.topLevelWidgets() but it returns a list with 4 widgets : the main window and 3 unexpected QMenu widgets.
  • I know I could use getParent() several times from the Matplotlib canvas but I am looking for a more robust solution: I don't want it to depend on the widget hierarchy.
  • I wonder if global variables would be a wise solution? I've never used them in Python.

Thank you in advance for any advice.

hug
  • 137
  • 1
  • 11
  • 2
    You may use model-view-controller pattern. – Dmitry Sazonov May 29 '17 at 07:56
  • I have never done that, would I been able to store Numpy arrays? The example I saw about that used SQL database. – hug May 29 '17 at 08:05
  • Global variables is typically a bad idea. If you need to share the same data among different objects you can consider a [Singleton](https://en.wikipedia.org/wiki/Singleton_pattern). – armatita May 29 '17 at 08:07
  • The model-view-controller pattern is frequently reduced in a view-controller pattern, since the model usually refers to frozen data. If you want to process dynamic data (as in generated at runtime), you only need your controller to be aware of this data. A usual structure is as follows: you have a module in PyQt that runs the graphical aspect, and has only graphical methods (like "display stuff"); a processing module, whose methods are aimed at processing the data ("compute stuff", "get stuff from file"); and a control module (probably `main.py`), that merely put these together. – Right leg May 29 '17 at 08:12
  • Thanks for the explaination. Can you indicate the classes I could use to achieve that? I fail to understand what concretely is the controller. – hug May 29 '17 at 08:33
  • @hug its really just a technical way of creating categories in your code that specialize in stuff like visualization, calculation, etc. Let me put into an answer a simple example of how to create a possible structure to obtain the same data anywhere in your code. – armatita May 29 '17 at 08:39

1 Answers1

0

It seems you need a centralized place to store your data which can be accessed by any number of different objects (Qt objects, numpy objects, it makes no difference). My suggestion is the Singleton pattern. I'll make a small experiment using Alex Martelli Borg implementation but check this resource (Python 3 Patterns, Recipes and Idioms) to see a few different examples.

I think the easiest way to explain a Singleton is the following: when you instantiate the same object multiple times (for instance a QWidget), each instance will refer to a different object. So if you make a change in one instance it will not affect the others (generally speaking). The Singleton works differently. Every single instance you make refers to the same object. Check the following example:

import numpy as np


class Singleton:
    """Alex Martelli implementation of Singleton (Borg)
    http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html"""
    _shared_state = {}

    def __init__(self):
        self.__dict__ = self._shared_state


class Database(Singleton):
    def __init__(self):
        Singleton.__init__(self)

    def get(self):
        return self._data

    def change(self):
        # Just arbitrary experiment of changing the data
        self.load()

    def load(self):
        # For example from csv or binary files using
        # np.genfromtxt or np.fromfile
        self._data = np.random.randint(0, 10, (3, 3))

With the code above I've just created my "Database" object. Every time I call this database and ask for its data it will return me the same. Notice how I'm making a randomized numpy array as data. Let's say I create two new objects:

class SomeObject:
    def __init__(self):
        self._something = None

    def doSomething(self):
        db = Database()
        print(db.get())


class OtherObject:
    def __init__(self):
        self._othersomething = None

    def doOtherSomething(self):
        db = Database()
        print(db.get())

Both call an instance of the Database. But will they refer to the same data? Lets check:

# Loading data into Singleton
db = Database()
db.load()

# Creating a couple of different objects
so = SomeObject()
oo = OtherObject()

# Making sure each object is calling the database
print("Original data in database:")
so.doSomething()
oo.doOtherSomething()

# Changing data in database
db.change()

# Checking the changes in both objects.
print("Changed data in database:")
so.doSomething()
oo.doOtherSomething()

The result is this:

Original data in database:
[[7 4 7]
 [3 4 1]
 [9 5 6]]
[[7 4 7]
 [3 4 1]
 [9 5 6]]
Changed data in database:
[[3 8 8]
 [7 2 8]
 [1 5 1]]
[[3 8 8]
 [7 2 8]
 [1 5 1]]

So both objects, creating their own instances of Database, obtain exactly the same data (which is randomized at the call of the load or change method). This is probably the most approachable (and maintainable) way of sharing data among different objects.

EDIT: To enable the database to understand if data has been loaded of not. Let's make a small modification to our Database class by adding the "hasData" method:

class Database(Singleton):
    def __init__(self):
        Singleton.__init__(self)

    def get(self):
        return self._data

    def change(self):
        # Just arbitrary experiment of changing the data
        self.load()

    def hasData(self):
        return hasattr(self, "_data")

    def load(self):
        # For example from csv or binary files using
        # np.genfromtxt or np.fromfile
        self._data = np.random.randint(0, 10, (3, 3))

This method is asking if Database has already the attribute _data or not. Let's make an experiment:

db = Database()
print(db.hasData())
db.load()
print(db.hasData())

Which results in:

False
True

Other variations could use the very first instantiation to load the data. This will depend greatly on the needs of your software.

armatita
  • 12,825
  • 8
  • 48
  • 49
  • Ok, I see, thank you very much for this detailed example. Just to be sure I understand it right, if I create a Database object in any widget, it will always refer to the same data as the first Database object I created (even if it was in another widget)? – hug May 29 '17 at 09:07
  • Yes. Just be careful of where you first load the data into the Database object (so that you do not try to use data that hasn't been loaded yet). You can do this right before creating your MainWindow (or Widget) object, for example. – armatita May 29 '17 at 09:12
  • In fact, I have to load data into it after the creation of my main window because the importation is done by the user with a QFileDialog. I use the data in MDI sub-windows. Is there an easy way to store the fact that a file has already been loaded (with a boolean member variable in Database for example)? – hug May 29 '17 at 09:27
  • Yes. The example of Singleton that I've used is not really prepared to have variable creation in __init__ (check the resource link for other ways to implement a Singleton if you need this) but a flag system would work. Also if you still want to use this kind of Singleton implementation there are ways. Check my edit. – armatita May 29 '17 at 09:32
  • Thanks, that's exactly what I need. One last thing concerning Singleton, if I understand it correctly, its definition implies that all the attributes we create (with self.[...] = [...]) are automatically static? – hug May 29 '17 at 10:03
  • No. The only difference between a Singleton and any other normal class is that all instantiations point to the same object. But all attributes of that object can be manipulated as in any other. The first example I gave has a line where I actually change the data midway into the code. You can load, save, change, modify, whatever you want inside a Singleton but this will reflect throughout all instantiations. Typically a static variable is permanent to the entire "life" of your program but the example above actually creates the `_data` variable "in" program. – armatita May 29 '17 at 10:10
  • The line I don't understand is "self.__dict__ = self._shared_state". – hug May 29 '17 at 10:21
  • `__dict__` is a default class attribute in Python which is a dictionary of all the class attributes (try doing `print(so.__dict__)`, for example, to see what I mean). That instruction is basically saying: "this class `__dict__` is always `_shared_state`". Notice __dict__ is an instance attribute. So all instances will have a `__dict___` which is `_shared_state`. – armatita May 29 '17 at 10:34
  • @hug It looks a bit confusing at first. I've looked in SO. Check [this question](https://stackoverflow.com/questions/12127925/why-is-this-python-borg-singleton-pattern-working) for further details about that specific question. – armatita May 29 '17 at 10:38
  • Ok, I will take a look. Thanks for everything, it was really helpful. – hug May 29 '17 at 10:49