pyCMD; простая оболочка для выполнения математических и Python команд

Я сделал командную строку на Python. Прежде чем я продолжу и добавлю другие команды, хорошо ли сделана моя программа?

Мне не нравится огромное количество утверждений «если» в нем. Но я не знаю лучшего способа сделать это. (JSON может быть использован позже, поэтому я включил его)

Вот код:

import time
import json
print("Welcome to pyCMD, usern")
time.sleep(0.2)
input("Press enter to startn")
time.sleep(0.2)

def SavetoDisk(data, file):
    with open(file, 'w') as outfile:
        json.dump(data, outfile)

def ReadfromDisk(file):
    with open(file) as json_file:
        data = json.load(json_file)
    return data
print('Starting command line, use "help . ." for help')
running = True
commandList = "add | sub | mult | div | exp | tetrate | python"
while running:
    try: 
        userInput = input(': ')
        tokens = userInput.split()
        command = tokens[0] 
        args = [(token) for token in tokens[1:]]
    except: print("unknown input error")
    try:
        arg1, arg2 = args
    except ValueError:
        print('Too many or too little args, 2 args required, if you want to not use an arg, use a "."')
    
    if command == "add":
        print(float(arg1) + float(arg2))
    
    if command == "sub":
        print(float(arg1) - float(arg2))
        
    if command == "mult":
        print(float(arg1) * float(arg2))        #math commands
        
    if command == "div":
        print(float(arg1) / float(arg2))
    
    if command == "exp":
        print(float(arg1) ** float(arg2))
        
    if command == "tetrate":
        for x in range(int(arg2)):
            arg1 = float(arg1) * float(arg1)
        print(arg1)
        
    if command == "python":
        exec(open(arg1).read())
        
    if command == "help":
        if arg1 == '.':
            if arg2 == '.':
                print('To see help about a command, type: "help [command] ." for list of commands type: "help command list"')
        
        if arg1 == 'command':
            if arg2 == 'list':
                print(commandList)
                
        if arg1 == 'add':
            print("Add: n Description: Adds 2 numbers n Syntax: add [num1] [num2]")
            
        if arg1 == 'sub':
            print("Sub: nDescription: Subtracts 2 numbers n Syntax : sub [num1] [num2]")
        
        if arg1 == 'mult':
            print("Mult: nDescription: Multiplies 2 numbers n Syntax : mult [num1] [num2]")
            
        if arg1 == 'div':
            print("Div: nDescription: Divides 2 numbers n Syntax : div [num1] [num2]")
        
        if arg1 == 'exp':
            print("Exp: nDescription: Raises 1 number by another n Syntax : exp [num1] [num2]")
            
        if arg1 == 'tetrate':
            print("Tetrate: nDescription: Tetration n Syntax : tetrate [num1] [num2]")
        
        if arg1 == 'python':
            print("Python: nDescription: Runs a python script n Syntax : python [path/to/program.py] .")

2 ответа
2

Первое, что я замечаю в вашем коде, это то, что он очень длинный глобальный скрипт. Чтобы решить эту проблему, я бы начал с перемещения вашего кода в functions.

Фактические расчеты могут быть очень простыми, например:

def add(arg1, arg2):
    print(float(arg1) + float(arg2))

Чтобы затем получить справочную информацию, мы могли бы добавить docstring.

def add(arg1, arg2):
    """
    Add:
     Description: Adds 2 numbers
     Syntax: add [num1] [num2]
    """
    print(float(arg1) + float(arg2))

Мы можем получить как строку документации, так и результат операции с Python. Чтобы строка документации была хорошо отформатирована, мы можем использовать textwrap.dedent.

>>> add("10", "5")
15.0
>>> import textwrap
>>> textwrap.dedent(add.__doc__.strip("n"))
Add:
 Description: Adds 2 numbers
 Syntax: add [num1] [num2]

Чтобы затем уменьшить количество строк кода, мы можем объединить все эти функции в словарь. И просто проиндексируйте словарь, чтобы получить конкретную функцию.

COMMANDS = {
    "add": add,
    "sub": sub,
    # ...
}

fn = COMMANDS["add"]
fn("10", "5")
15.0

Хотя вы можете создать словарь и самостоятельно выполнять команды, вместо этого вы можете создать подкласс cmd.Cmd. Вам нужно будет немного изменить функции, чтобы в качестве входных данных использовалась только строка, а префикс do_ к любым командам, доступным в командной строке.

import cmd


class PyCMD(cmd.Cmd):
    intro = 'Welcome to pyCMD, usernStarting command line, use "help" for help'
    prompt = ": "

    def do_add(self, arg):
        """
        Add:
         Description: Adds 2 numbers
         Syntax: add [num1] [num2]
        """
        arg1, arg2 = arg.split()
        print(float(arg1) + float(arg2))

PyCMD().cmdloop()
Welcome to pyCMD, user
Starting command line, use "help" for help
: help

Documented commands (type help <topic>):
========================================
add  help

: help add

        Add:
         Description: Adds 2 numbers
         Syntax: add [num1] [num2]
        
: add 10 5
15.0

Помощь явно немного сломана. Чтобы исправить это, мы можем изменить строку документации после функции.

class PyCMD(cmd.Cmd):
    def do_add(self, arg):
        # ...

    do_add.__doc__ = textwrap.dedent(do_add.__doc__.rstrip(" ").strip("n"))

Это немного некрасиво, и делать это для каждой функции было бы ужасно. Итак, мы можем создать функцию для этого.

def clean_doc(fn):
    fn.__doc__ = textwrap.dedent(fn.__doc__.rstrip(" ").strip("n"))


class PyCMD(cmd.Cmd):
    def do_add(self, arg):
        # ...

    clean_doc(do_add)

Это все еще немного уродливо, поэтому мы можем использовать @ сделать это для нас. Это называется декоратор. Это имеет смысл, потому что мы украшаем то, как __doc__ виден. Обратите внимание, что мы изменили clean_doc вернуться fn.

def clean_doc(fn):
    fn.__doc__ = textwrap.dedent(fn.__doc__.rstrip(" ").strip("n"))
    return fn


class PyCMD(cmd.Cmd):
    @clean_doc
    def do_add(self, arg):
        # ...

Мы можем добавить еще одну функцию, например clean_doc, но на этот раз сделайте это закрытие если вы хотите легко добавить проверку двух аргументов.

import functools


def command(fn):
    @functools.wraps(fn)
    def wrapper(self, arg):
        args = [token for token in arg.split()]
        try:
            arg1, arg2 = args
        except ValueError:
            print('Too many or too little args, 2 args required, if you want to not use an arg, use a "."')
        else:
            return fn(self, arg1, arg2)
    return wrapper


class PyCMD(cmd.Cmd):
    @command
    @clean_doc
    def do_add(self, arg1, arg2):
        """
        Add:
         Description: Adds 2 numbers
         Syntax: add [num1] [num2]
        """
        print(float(arg1) + float(arg2))

import cmd
import textwrap
import functools


def command(fn):
    @functools.wraps(fn)
    def wrapper(self, arg):
        try:
            arg1, arg2 = arg.split()
        except ValueError:
            print('Too many or too little args, 2 args required, if you want to not use an arg, use a "."')
        else:
            return fn(self, arg1, arg2)
    return wrapper


def clean_doc(fn):
    fn.__doc__ = textwrap.dedent(fn.__doc__.rstrip(" ").strip("n"))
    return fn


class PyCMD(cmd.Cmd):
    intro = 'Welcome to pyCMD, usernStarting command line, use "help" for help'
    prompt = ": "

    @command
    @clean_doc
    def do_add(self, arg1, arg2):
        """
        Add:
         Description: Adds 2 numbers
         Syntax: add [num1] [num2]
        """
        print(float(arg1) + float(arg2))


PyCMD().cmdloop()
Welcome to pyCMD, user
Starting command line, use "help" for help
: help

Documented commands (type help <topic>):
========================================
add  help

: help add
Add:
 Description: Adds 2 numbers
 Syntax: add [num1] [num2]
: add 10 5
15.0

Остальные команды оставлены вам для реализации

  • 2

    Что хорошего в этом ответе, так это разделение внешнего поведения (запрос строки, разделение, проверка количества слов, поддержка help command) из внутреннего представления возможных команд. Внутреннее представление идиоматические функции Python 👏. Это широко применимая идея, и она почти всегда проясняет оба аспекта, чем когда они смешаны.

    — Бени Чернявский-Паскин

  • Это очень хорошо написанный ответ. Мне нравится, как вы превратили код в красивый расширяемый фреймворк для максимально простого добавления будущих команд. Я тоже люблю декораторов. Я определенно кое-что узнал из этого ответа, хотя считал себя очень свободно владеющим питоном и питоническим дизайном!

    — викарджрамун

Отзыв по главному вопросу: уменьшаем если

Позвольте мне начать с того, что Python не поддерживает switch заявления, которые были бы очень полезны в этом случае. Я бы рекомендовал прочитать это Ответ StackOverflow о том, как получить аналогичный результат (здесь я тоже буду использовать этот подход).

Вы должны обратить внимание на то, что ваши операторы if:

if command == "add":
    print(float(arg1) + float(arg2))

запускают код, который следует этому шаблону:

print(arg operand arg)

Отсюда следует вопрос: как это обобщить?

Оказывается, мы можем использовать operator в наших интересах здесь:

import operator
def operation(a, b, operand):
   return operand(a, b)

Затем вы можете сопоставить свои «команды» с разными операндами как таковыми:

def get_operator(x):
   return {
     'add': operator.add,
     'multiply': operator.mul,
     ...
   }[x]

и, наконец, вы можете уменьшить количество «если» с помощью одной строчки кода:

operation(arg1, arg2, get_operator(command))

Следует отметить, что вы также могли написать:

get_operator(command)(arg1, arg2)

Можно утверждать, что один подход лучше другого по разным причинам, связанным с абстракциями, декомпозицией и т. Д.

Общий обзор

Связать

Вместо того, чтобы делать всю эту токенизацию команд самостоятельно, вы должны использовать argparse модуль. Я не собираюсь объяснять здесь, как преобразовать ваш код для использования argparse, но если вы будете следовать руководству в документации, то это должно быть несложно. Это также значительно сократит объем кода, который вы написали для меню справки.

Именование

В Python рекомендуется использовать snake_case для именования переменных и функций. Итак, функция SavetoDisk станет save_to_disk.

Списки и перечисления

Ваш commandList это не список. Это просто строка, разделенная | характер. Лучше подумайте об использовании истинного списка и перечисляет а не строки для ваших команд.

commands = [ Command.ADD, Command.MULTIPLY, ...]

Удачи!


Обновление сопоставления с шаблоном Python

Я только что узнал, что предложения по сопоставлению с образцом (PEP 636 и товарищи PEP 634, PEP 635) и только вчера были приняты в разработку 02.08.2021!

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *