1

I'm a relative new comer to Python (but am experienced in many other languages!) and am writing a Python script to process scientific measurement data.

I've ended up with a number of class functions that each call matplotlib.pyplot.plot(). I include a simple example below:

def plot_measurement(self, x, x_label, y, y_label, plot_label = "", format = "-b", line_width = 1, latex_mode = False):
    if (plot_label == ""):
        plot_label = self.identifier

    if (latex_mode):
        matplotlib.rc("text", usetex = True)
        matplotlib.rc("font", family = "serif")

    matplotlib.pyplot.plot(x, y, format, linewidth = line_width, label = plot_label)
    matplotlib.pyplot.xlabel(x_label)
    matplotlib.pyplot.ylabel(y_label)

I would like to be able to add all of the matplotlib.pyplot.plot() parameters to my new functions so that I may feed them into matplotlib.pyplot.plot() but don't wish to do so manually (by adding them to the function declaration), which you'll see from that code snippet that I have already done so in some cases. The crux is that each new function has it's own set of parameters that somehow need to be distinguished from the parameters to matplotlib.pyplot.plot().

A little bit of searching online led me to discovering Python decorators but I have not been able to find a good example that will help me in this instance. I'm convinced there is an easy way to do this in Python.

If someone could please help me with this I would be most grateful.

Optimist
  • 93
  • 1
  • 5
  • It's not so much about decorators, but variable argument lists? I suggest to use keyword arguments, as described e.g. here: https://realpython.com/python-kwargs-and-args/ – Dr. V Jun 10 '20 at 16:19
  • Thanks for your comment, Dr V. I had found that page already but it wasn't clear to me how to separate new parameters from those to be passed to matplotlib.pyplot.plot(). If you could provide an example of how to change the function declaration in the code snippet that would be superb. – Optimist Jun 10 '20 at 16:23

3 Answers3

2

You can use args and kwargs in your function signature and pass through arguments to the plot() function. There are a lot of excellent explanations explaining how they work, so I won't try to repeat it all here.

Essentially args and kwargs allow you to pass a variable number of arguments. In the case of kwargs it packs up any 'extra' keyword arguments you pass to the function in a dictionary. The dictionary can then be passed inside the receiving function and unpacked with **kwargs

For your function:

def plot_measurement(x_label, y_label, *args, latex_mode = False, **kwargs):
    # Keyword arguments can be accessed as a normal dictionary
    if (kwargs["label"] == ""):
        kwargs["label"] = self.identifier

    if (latex_mode):
        matplotlib.rc("text", usetex = True)
        matplotlib.rc("font", family = "serif")

    matplotlib.pyplot.plot(*args, **kwargs)
    matplotlib.pyplot.xlabel(x_label)
    matplotlib.pyplot.ylabel(y_label)

Call it using the function arguments and adding any extra arguments you need for plot():

plot_measurement("x_label", "y_label", x, y, latex_mode = False, linewidth = 1, label = "plot_label")

args and kwargs will 'soak up' any extra arguments you pass to your function. To use your keyword argument, place it after all the positional arguments in the function signature - which now includes *args.

Full working example:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt

def plot_measurement(x_label, y_label, *args, latex_mode = False, **kwargs):
    if (kwargs["label"] == ""):
        kwargs["label"] = self.identifier

    if (latex_mode):
        matplotlib.rc("text", usetex = True)
        matplotlib.rc("font", family = "serif")

    plt.plot(*args, **kwargs)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.show()

x = np.arange(0, 20)
x = np.reshape(x, (4, 5))
y = np.arange(5, 25)
y = np.reshape(y, (4, 5))

plot_measurement("x axis label", "y axis label", x, y, latex_mode = False, color = "red", label = "plot label")

Produces: enter image description here

Erik White
  • 336
  • 2
  • 8
  • Thanks for your comment Erik. This has been helpful. I have been trying to get it to work but haven't yet managed however. Why don't you also include *args in the function declaration? I want to pass to matplotlib.pyplot.plot() two numpy.arrays in *args but notice that they get concatenated. Any ideas? – Optimist Jun 10 '20 at 17:13
  • Sorry I missed that `plot()` needs positional arguments. I will update my answer with a working example – Erik White Jun 11 '20 at 09:04
0

To expand upon @Dr. V 's comment, you can pass a single dictionary of parameters to plot_measurement with all positional arguments and a second with all optional arguments to keep things simpler. Traditionally, these are called args and kwargs (key word args). Using a * as in *args unrolls a list and puts each list element into the function as an argument; similarly, ** unrolls a dictionary and puts each dictionary key-value pair into the function (which is convenient for keyword arguments)

# also this is standard because it's very convenient
import matplotlib.pyplot as plt

## Examples of how args and kwargs are formatted 

# all required arguments go in a list in order
args = [x,y,format]

# all non-required (keyword) arguments go in a dictionary
kwargs = {
     line_width: 1,
     label: plot_label
     }


def plot_measurement(self,args,kwargs,plot_label,x_label,y_label,latex_mode = False):
    # here all of the args and keyword args are passed together
    # whereas all arguments used directly by plot_measurement are not passed together
    # though they could be for cleanliness

    if (plot_label == ""):
        plot_label = self.identifier

    if (latex_mode):
        matplotlib.rc("text", usetex = True)
        matplotlib.rc("font", family = "serif")

    plt.plot(*args, **kwargs)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
DerekG
  • 3,555
  • 1
  • 11
  • 21
  • Thanks for your comment. It looks like using args and kwargs is the way to go. I am not having much luck getting it working at the moment however. Am trying to debug/produce a test case and will report back. – Optimist Jun 10 '20 at 17:15
0

For posterity the code posted in this response DOES NOT WORK and is a small test case in response to @Derek and @Erik.

I couldn't see how to put formatted code in the comment so am posting it here. Please forgive my sins!

def plot_measurement(self, latex_mode = False, *args, **kwargs):
    print("\nlen(args) = {0}, args = {1}".format(len(args), args))
    print("\nlen(kwargs) = {0}, kwargs = {1}\n".format(len(kwargs), kwargs))

    if (latex_mode):
        matplotlib.rc("text", usetex = True)
        matplotlib.rc("font", family = "serif")

    matplotlib.pyplot.plot(*args, **kwargs)

Called using the following incantation.

test_measurement1.plot_measurement(test_measurement1.data[6], test_measurement1.data[15])

Both data[6] and data[15] are numpy.arrays and get concatenated together. Output below:

len(args) = 1, args = (array([-8.21022986e-06, -8.19599736e-06, -8.16865495e-06, ...,
       -7.70015886e-06, -7.70425522e-06, -7.71744717e-06]),)

len(kwargs) = 0, kwargs = {}

Also, the code errors on the line

if (latex_mode):

giving the error

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
Optimist
  • 93
  • 1
  • 5
  • Does *args need to come before the named parameter (key-word argument) latex_mode? Seems to fix the error and the code now works but am not sure why. – Optimist Jun 10 '20 at 17:40
  • `*args` are positional arguments, so your keyword argument `latex_mode` needs to go after all the positional arguments as usual. I have updated my answer showing how to use it. – Erik White Jun 11 '20 at 09:21
  • Thanks for your help Erik. This now works as intended. I do find it odd that positional arguments can't be put between keyword arguments as in doing so the order of the positional arguments is still well defined. – Optimist Jun 11 '20 at 11:13