0

I am creating a python CLI, where user can provide an operation they want to perform, for eg:

sum 10 15

In my code, I have defined my classes as follows:

class Operation:
    # common stuff
    pass

class Sum(Operation):
    identifier = "sum"
    def perform(a, b):
        return a + b

class Difference(Operation):
    identifier = "diff"
    def perform(a, b):
        return a - b

Now, in my CLI, if I type sum 10 15 I want to return the result of Sum.perform(10, 15) and similarly if I type diff 10 15, I return the result of Difference.perform(10, 15), as sum is the identifier of class Sum and diff is the identifier of class Difference.

How do I dynamically access the class and its perform method, when I get the input directly from user input?

martineau
  • 119,623
  • 25
  • 170
  • 301
shubham
  • 53
  • 2
  • 5
  • 1
    Have a look at the `cmd` module in the standard library. – chepner Jan 21 '22 at 16:07
  • I am using the `cmd` module to get the string `sum` and `diff` and the arguments `a` and `b`. My question is how do I go from the string `sum` and return the result of `Sum.perform(a, b)`. – shubham Jan 21 '22 at 16:10
  • Please show your code. The entire purpose of the `cmd` module is to map an input like `sum 10 15` to a function that takes 2 arguments. – chepner Jan 21 '22 at 16:18

2 Answers2

2

Classes in Python are first-class citizens, meaning they can be used as standard objects. In particular we can simply store them in a dictionary:

my_dict = {
    'sum': Sum,
    'diff': Difference,
}

and so on. Then when you get the operation name as string from command line you simply do

my_dict[op_name].perform(a, b)

Note that this is a very basic (and you will soon see problematic, e.g. not all operators accept two arguments) approach to what is known as parsing and abstract syntax trees. This is a huge topic, a bit hard but also very interesting. I encourage you to read about it.

// EDIT: If you want to keep identifier on the class, then you can apply a simple class decorator:

my_dict = {}

def autoregister(cls):
    # It would be good idea to check whether we
    # overwrite an entry here, to avoid errors.
    my_dict[cls.identifier] = cls
    return cls

@autoregister
class Sum(Operation):
    identifier = "sum"
    def perform(a, b):
        return a + b

print(my_dict)

You have to remember though to import all classes before you use my_dict. In my opinion an explicit dict is easier to maintain.

freakish
  • 54,167
  • 9
  • 132
  • 169
  • In this approach, I'd have to maintain a dictionary mapping the identifier to the class. However, I'm looking for a scalable approach, that does not require me to maintain a separate dictionary. Is it possible via the base Operation class, or by defining a metaclass? – shubham Jan 21 '22 at 16:29
  • 1
    First of all, you won't have millions of operators, more like few tens at most. So you don't have to worry about scalability. Secondly, sure, you can keep the operator name on the class and autoregister them into some shared dict via class decorator. I'm not sure if it is worth it though. Anyway, I've updated the answer. – freakish Jan 21 '22 at 16:33
  • thanks a lot. this seems to solve my issue. – shubham Jan 21 '22 at 16:38
  • `my_dict` isn't a class, it's an *instance* of the `dict` class, so your initial statement is irrelevant (and somewhat misleading because generally classes *themselves* aren't used to store data — their instances might). – martineau Jan 21 '22 at 16:55
  • @martineau where exactly did I say that `my_dict` is a class? The initial statement refers to OP's classes, I explicitly say that they can be stored in a dict. I don't see anything misleading in my answer. – freakish Jan 22 '22 at 07:48
  • @martineau and generally classes can (and it seems that's OP's case) be used to store data. Whether this is a good design is a different question. – freakish Jan 22 '22 at 07:55
  • Classes themselves are generally **not** used to store data, although *instances* of the often are. I think it's important to make the distinction clear since classes are "[first-class-citizens](https://en.wikipedia.org/wiki/First-class_citizen)" in Python (unlike many other languages). – martineau Jan 22 '22 at 08:11
  • @martineau do we talk about other languages here? I don't see how your "important distinction" is relevant in the context. In Python classes can and are used to store data. Btw I already linked the wiki for first-class citizens in my answer. I have a feeling that you didn't read my answer thoroughly. – freakish Jan 22 '22 at 09:30
0

Reading your comment, I think you need to interpret the input. The way I would go about this is splitting the input by spaces (based on your example), and then checking that list. For example:

# This is the place you called the input:
input_unsplit = input("Enter your command and args")
input_list = input_unsplit.split(" ")

# Check the first word to see what function we're calling
if("sum") in input_list[0].lower():
    result = Sum.perform(input_list[1], input_list[2])
    print(result)
# this logic can be applied to other functions as well.

This is a simple solution that could be hard to scale.

=== EDITED ===

I have more to add.

If used correctly, dir() can make a list of defined classes up to a certain point in the code. I wrote a calculator for my precal class, and in it I chose to use dir after defining all the math classes, and then if the name met certain conditions (i.e not main), it would be appended to a list of valid args to pass. You can modify your classes to include some kind of operator name property:

def class Addition:
    self.op_name = "sum"

and then perform to take in an array:

    def perform(numbers):
        return numbers[0] + numbers [1]

To solve many of your scalability issues. Then, after declaring your classes, use dir() in a for loop to append to that valid array, like so:

valid_names = []
defined_names = dir()
for name in defined_names:
    if '_' not in name:
        if name not in ("sys","argparse","<any other imported module/defined var>"):
            valid_names.append(name)

Note that making this step work for you is all in the placement in the script. it's a bit tedious, but works flawlessly if handled correctly (in my experience).

Then, you can use eval (safe in this context) to call the method you want:

# get input here

for name in defined_names:
    if eval(name).op_name == input_list[0].lower():
        eval(name).perform(input_list)

This should be a fairly easy-to-scale solution. Just watch that you keep the dir check up to date, and everything else just... works.

Cameron Bond
  • 111
  • 5
  • Yes this would be a hard to scale because I'd want to add multiple other Operations as well, as the need arises. The Sum and Difference are just used as an example. – shubham Jan 21 '22 at 16:25
  • Does this fit the original issue a little better? All you have to maintain is the dir() check, and then just write the classes. – Cameron Bond Jan 21 '22 at 17:00