Как преобразовать простой скрипт командной строки в объектно-ориентированный?

У меня нет большого опыта работы с объектно-ориентированным Python и вы хотите реорганизовать простой инструмент командной строки. Мой текущий скрипт просто импортирует необходимые библиотеки, имеет несколько определений функций, использует глобальные переменные (которые, как я знаю, является плохой практикой) и использует argparse all в одном .py файле, например:

import argparse dict = {} #Some code to populate the dict, used both in checking the argument and later in the script def check_value(value): if not dict.has_key(value): raise argparse.ArgumentTypeError("%s is invalid." % value) return value parser = argparse.ArgumentParser(…) parser.add_argument('…', type=check_value, help='…') args = parser.parse_args() # Some other code that uses the dict 

Кроме того, способ, которым я обрабатываю некоторый синтаксический анализ аргумента, использует функцию, аналогичную функции «abc», которая изменяет словарь, который мне понадобится позже в скрипте. Как сделать это менее уродливым? Или использует глобальные переменные, приемлемые для простого сценария командной строки?

Давайте создадим один класс, который имитирует наш dict, и get передается этим методам вместо работы с глобальной переменной, и вы можете сказать мне, что это больше того, что вы ищете. Дело в том, что вы уже используете немного объектно-ориентированного программирования и не знаете его (класс dict)

Шаг 1: Класс Дикта

Объектно-ориентированное программирование на Python вращается вокруг классов, которые могут быть созданы экземплярами один или несколько раз, и они используют функции, определенные на них. Затем мы вызываем методы с classname.class_method(input_variables) и получаем возвращаемое значение так же, как если бы мы вызывали функцию без привязки. На самом деле существует четко определенная разница между глобальной функцией и функцией, явно связанной с экземпляром класса. В этом разница между методом «bound» и «unbound», и именно там мы получаем магическое имя.

 class ExampleDict(object): #Called when a new class instance is created def __init__(self, test): self.dict = {} self.dict["test"] = test #Called when len() is called with a class instance as an argument def __len__(self): return len(self.dict) #Called when the internal dict is accessed by instance_name[key] def __getitem__(self, key): return self.dict[key] #Called when a member of the internal dict is set by #instance_name[key] = value def __setitem__(self, key, value): self.dict[key] = value #Called when a member of the internal dict is removed by del instance[key] def __delitem__(self, key): del self.dict[key] #Return an iterable list of (key, value) pairs def get_list(self): return self.dict.items() #Replace your global function with a class method def check_value(self, key): if not self.dict.has_key(value): raise argparse.ArgumentTypeError("%s is invalid." % value) return value 

Несколько примечаний:

  • Это определение класса должно появляться до создания любых экземпляров
  • Экземпляр класса создается со следующим синтаксисом: d = ExampleDict()

Шаг 2: Удаление глобальных переменных

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

 def check_value_global(inp_dict, value): if not inp_dict.has_key(value): raise argparse.ArgumentTypeError("%s is invalid." % value) return value 

Шаг 3: Объявление экземпляров класса и использование их

Теперь давайте определим, что происходит, когда мы запускаем скрипт и объявляем некоторые экземпляры класса, затем передаем их методу или выполняем их методы класса:

 if __name__ == "__main__": #Declare an instance of the class ex = ExampleDict("testing") print(ex["test"]) ex["test2"] = "testing2" check_value_global(ex, "test") print("At next section") ex.check_value("test2") print("At final section") , if __name__ == "__main__": #Declare an instance of the class ex = ExampleDict("testing") print(ex["test"]) ex["test2"] = "testing2" check_value_global(ex, "test") print("At next section") ex.check_value("test2") print("At final section") 

Более подробное обсуждение возможностей объектно-ориентированного программирования в Python см. Здесь.

Изменить на основе комментария

Хорошо, давайте посмотрим, в частности, на argparse. Это приведет к анализу аргументов командной строки, переданных в скрипт (альтернатива здесь просто будет считываться из sys.argv).

Теоретически мы могли бы включить это в любом месте, но мы действительно должны включить его либо непосредственно после, if __name__=="__main__": либо в методе, вызванном после этого утверждения. Зачем?

Все, что после этого утверждения запускается, только если скрипт python вызывается непосредственно из командной строки, а не импортируется в качестве модуля. Предположим, вы хотели импортировать скрипт в другой скрипт python и использовать его там. В этом случае вам не нужны аргументы командной строки, поэтому вы не хотите их анализировать.

Со всем этим мы теперь знаем, что у нас есть как объект dict, так и объект argparse, инициализированный в основном сегменте (после if __name__=="__main__": . Как передать их функциям и классам, определенным выше в программе?

Ну, у нас есть много вариантов, наиболее распространенными я использую:

  • В случае необходимости мы можем переопределить класс dict, который мы используем, чтобы позволить методам вызывать методы класса после инициализации
  • Мы можем передавать объекты в функции, как показано на шаге 2 выше
  • Мы можем определить одноэлементный класс, который принимает аргументы объекта dict и argparse и сохраняет их. Затем весь основной поток программы в соответствующей области проходит через синглтон, и эти ссылки всегда доступны

Вот пример:

 class SingletonExample(object): def __init__(self, dict_obj, arg_obj): self.dict = dict_obj self.args = arg_obj def some_script_function(self): pass #Use your self.dict and self.args arguments 

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

Вероятно, вы захотите сосредоточиться на инъекции зависимостей. В основном передайте все, что вам нужно использовать в своих объектах / функциях.

Мне нравится иметь фабричную функцию args_to_options() . Он возвращает класс « Options » (в Python, возможно, будет прав) с установленными флагами и свойствами или ошибками, если есть проблема. Удостоверьтесь, что он просто отвечает за построение опций и ничего больше. У вас может даже быть отдельная функция проверки, если вы хотите.

Обратите внимание, что мой Python немного ржавый, так что будьте осторожны.

 class App: def __init__(self, options): self.apply_options(options) def apply_options(self, options): if options.do_the_thing: self.thing_dooer = true # or maybe just self.options = options def run(self): if self.thing_dooer: do_thing() class Options: do_the_thing = False server_address = None def validate(self): # if it's in a bad state, raise exception # (or return a bool you check) if self.server_address is None: # raise here def args_to_options(args): # Parse arguments here # raise exception if parsing fails return options def Main(stdout, stderr, args, file_opener): try: options = args_to_options(args) options.stdout = stdout options.stderr = stderr options.validate() except: print_error(file=stderr) print_help(file=stdout) app = App(options, file_opener) app.run() if __name__ == "__name__": Main(sys.stdout, sys.stderr, sys.argv, open) 

На самом деле я делаю отдельную Main функцию, которая вызывается из реального main (в Python «реальная главная» будет в проверке имени if). Я буду передавать stdout , stdin , stderr мере необходимости и рассматривать их как общие потоки чтения / записи. Возможно, общий локальный интерфейс файловой системы (некоторый класс, который обертывает open() , os.link , os.mkdir мере необходимости). В Go я использую AferoFS (хотя он не поддерживает привязку). Если у вас есть много чего, то пользовательский класс, чтобы держать все это.

Тогда я не буду использовать отпечатки без указания записи потока, и все вызовы open() моего файла проходят через переданный интерфейс.

Многие из них, вероятно, слишком переполнены, особенно в Python, где вы можете обезьяны исправлять реализации таких вещей, как open и stdout, но это шаблон, который я использую в других местах и ​​позволяющий тестировать.

В дополнение к тестированию вы получаете гибкость, чтобы изменить то, что делает ваш код. Например, он может передавать файлы через http, заменяя open() ).

Вы также можете установить stdout / stderr непосредственно в опциях, а не в классе App.

Часто лучше использовать абстракции более высокого уровня для интерфейсов. Например, вместо того, чтобы передавать в приложение stdout / stderr приложение, журнал может быть лучше. Вместо интерфейса файловой системы низкого уровня, общий «DataStore», который обрабатывает имена файлов как ключ для поиска значения и возвращает фактический объект. Базы данных выполняются практически так же.

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

Даже тогда его стоит отступить и думать о более «родовой» концепции того, что вы делаете. Вы создаете каталоги? или добавление категорий в библиотеку, которая просто хранится в формате каталога. Под капотом это будет реализовано так же, но архитектура будет более общей и чистой. Это позволит вам отделить обработку / синтаксический анализ ввода-вывода от логики вашего приложения.

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