Идеальная настройка вашего Golang проекта
- Часть 1
Идеальная настройка вашего Golangпроекта
При запуске нового проекта самые большие трудности у меня всегда вызывала его настройка. Всегда стараешься сделать её «идеальной»:
- используешь лучшую структуру каталогов, чтобы всё было легко найти и импортирование происходило без проблем;
- настраиваешь все команды так, чтобы нужные действия выполнялись в один клик или с вводом одной команды;
- находишь лучший инструмент контроля качества кода, средство форматирования, среду тестирования для используемого в проекте языка и библиотеки…
Этот список можно продолжать и продолжать, и всё равно до идеальной настройки будет ещё далеко… Но, по моему скромному мнению, эта настройка для Golang просто лучшая!
Она так хорошо себя проявляет отчасти и потому, что основана на существующих проектах, которые вы можете найти здесь и тут.
Краткое изложение доступно в моём репозитории — https://github.com/MartinHeinz/go-project-blueprint Структура каталогов
Первым делом обратимся к структуре каталогов нашего проекта. Здесь у нас несколько файлов верхнего уровня и четыре каталога:
pkg — это пакет Go, который содержит только строку версии global. Меняется на версию из хэша текущей фиксации при проведении сборки; config — конфигурационный каталог, который содержит файлы со всеми необходимыми переменными среды. Вы можете использовать любой тип файла, но я бы рекомендовал файлы YAML: их проще читать; build — в этой директории у нас все скрипты оболочки, необходимые для сборки и тестирования приложения, а также создания отчётов для инструментов анализа кода; cmd — фактический исходный код. По правилам именования исходный каталог называется cmd. Внутри есть ещё один каталог с именем проекта (в нашем случае blueprint). В свою очередь, внутри этого каталога находится main.go, запускающий всё приложение. Также здесь можно найти все остальные исходные файлы, разделённые на модули (подробнее об этом далее).
Оказывается, многие предпочитают помещать исходный код в каталоги internal и pkg. Я думаю, что это лишнее: достаточно использовать для этого cmd, где для всего есть своё место.
Помимо каталогов, есть ещё большое количество файлов, о которых мы поговорим в статье. Модули Go для идеального управления зависимостями
В проектах Go используются самые разные стратегии управления зависимостями. Однако с версии 1.11 Go обзавёлся официальным решением. Все наши зависимости приводятся в файле go.mod, в корневом каталоге. Вот как он может выглядеть:
module github.com/MartinHeinz/go-project-blueprint go 1.12 require ( github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.4.0 )
Вы можете спросить: «А как в этот файл включить зависимости?». Очень просто, всего одной командой:
go mod vendor
Эта команда переустанавливает vendor каталог основного модуля для включения всех пакетов, необходимых для сборки и тестирования каждого пакета модуля исходя из состояния файлов go.mod и исходного кода Go. Фактический исходный код и конфигурация
И вот наконец мы добрались до исходного кода. Как уже говорилось, исходный код разделён на модули. Модуль представляет собой каталог внутри исходного корневого каталога. В каждом модуле находятся исходные файлы вместе с соответствующими файлами тестов. Например:
./cmd/ └── blueprint ├── apis <- Module │ ├── apis_test.go │ ├── user.go │ └── user_test.go ├── daos <- Module │ ├── user.go │ └── user_test.go ├── services <- Module │ ├── user.go │ └── user_test.go ├── config <- Module │ └── config.go └── main.go
Такая структура способствует лучшей читаемости и лёгкости сопровождения кода: он идеально разделён на части, которые проще просматривать. Что касается конфигурации, в этой настройке используем библиотеку конфигураций Go Viper, которая может иметь дело с разными форматами, параметрами командной строки, переменными среды и т.д.
Посмотрим, как мы используем этот Viper здесь. Вот пакет config:
var Config appConfig type appConfig struct { // Пример переменной, загружаемой в функции LoadConfig ConfigVar string } // LoadConfig загружает конфигурацию из файлов func LoadConfig(configPaths ...string) error { v := viper.New() v.SetConfigName("example") // <- имя конфигурационного файла v.SetConfigType("yaml") v.SetEnvPrefix("blueprint") v.AutomaticEnv() for _, path := range configPaths { v.AddConfigPath(path) // <- // путь для поиска конфигурационного файла в } if err := v.ReadInConfig(); err != nil { return fmt.Errorf("failed to read the configuration file: %s", err) } return v.Unmarshal(&Config) }
Он состоит из единственного файла. Объявляет один struct, который содержит все переменные конфигурации и имеет одну функцию LoadConfig, которая загружает конфигурацию. Требуется путь до конфигурационных файлов, в нашем случае используем путь до каталога config, который находится в корневом каталоге проекта и содержит наши YAML файлы. И как их будем использовать? Запустим первым делом в main.go:
if err := config.LoadConfig("./config"); err != nil { panic(fmt.Errorf("invalid application configuration: %s", err)) }
Простое и быстрое тестирование
Что важнее всего после кода? Тесты. Чтобы писать много хороших тестов, нужна настройка, с которой это будет делать легко. Для этого мы используем цель Makefile под названием test, которая собирает и выполняет все тесты в подкаталогах cmd (все файлы с расширением _test.go). Эти тесты кэшируются, так что их запуск происходит только при наличии изменений в соответствующей части кода. Это очень важно: если тесты будут слишком медленными, рано или поздно вы перестанете их запускать и сопровождать. Помимо модульного тестирования, make test помогает следить за общим качеством кода, запуская с каждым тестовым прогоном gofmt и go vet. gofmt способствует правильному форматированию кода, а go vet помогает с помощью эвристических алгоритмов выявлять в коде любые подозрительные конструкции. Вот пример того, что может получиться в результате выполнения:
foo@bar:~$ make test Running tests: ok github.com/MartinHeinz/go-project-blueprint/cmd/blueprint (cached) ? github.com/MartinHeinz/go-project-blueprint/cmd/blueprint/config [no test files] ? github.com/MartinHeinz/go-project-blueprint/pkg [no test files] Checking gofmt: FAIL - the following files need to be gofmt'ed: cmd/blueprint/main.go Checking go vet: FAIL # github.com/MartinHeinz/go-project-blueprint/cmd/blueprint cmd/blueprint/main.go:19:7: assignment copies lock value to l: sync.Mutex Makefile:157: recipe for target 'test' failed make: *** [test] Error 1 Запуск всегда в Docker
Многие говорят, что у них запуск невозможен в облаке, а только на компьютере. Здесь есть простое решение: всегда запускаться в контейнере docker. Делаете ли вы сборку, запускаете ли или тестируете — делайте всё это в контейнере. Кстати, что касается тестирования, make test выполняется тоже только в docker.
Посмотрим, как это происходит. Начнём с файлов Dockerfile из корневого каталога проекта: один из них для тестирования (test.Dockerfile), а другой — для запуска приложения (in.Dockerfile):
test.Dockerfile — в идеале нам было бы достаточно одного файла Dockerfile для запуска приложения и тестирования. Но во время тестовых прогонов нам может потребоваться внести небольшие изменения в среде выполнения, поэтому у нас здесь есть образ для установки дополнительных инструментов и библиотек. Предположим, например, что мы подключаемся к базе данных. Нам не нужно поднимать весь PostgreSQL-сервер при каждом тестовом прогоне или зависеть от какой-нибудь базы данных на хост-машине. Мы просто используем для тестовых прогонов базу данных в памяти SQLite. И если дополнительные установки не понадобятся нашим тестам, то двоичным данным в SQLite они будут очень даже кстати: устанавливаем gcc и g++, переключаем флажок на CGO_ENABLED, и готово. in.Dockerfile — если посмотреть на этот Dockerfile в репозитории, что мы увидим: просто несколько аргументов и копирование конфигурации в образ. Но что здесь происходит? in.Dockerfile используется только из Makefile (заполненного аргументами), когда мы запускаем make container. Давайте теперь обратимся в сам Makefile, Всё, что связано с docker, делает для нас именно он. 👇
Связываем всё вместе с помощью Makefile
Долгое время Make-файлы казались мне страшными (до этого я сталкивался с ними лишь при работе с кодом C), но на самом деле ничего страшного здесь нет, и их много где можно использовать, в том числе для этого проекта! Посмотрим, какие цели у нас здесь есть в Makefile:
make test — первая в рабочем потоке — собранное приложение — создаёт исполняемый двоичный код в каталоге bin: @echo "making $(OUTBIN)" @docker run \ # <- Это `докерный запуск` -i \ # скрытая команда --rm \ # <- Удаляем контейнер по завершении -u $$(id -u):$$(id -g) \ # <- Используем текущего пользователя -v $$(pwd):/src \ # <- Подключаем исходную папку -w /src \ # <- Устанавливаем рабочий каталог -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ # <- Подключаем каталоги -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ # с выводом двоичных данных -v $$(pwd)/.go/cache:/.cache \ --env HTTP_PROXY=$(HTTP_PROXY) \ --env HTTPS_PROXY=$(HTTPS_PROXY) \ $(BUILD_IMAGE) \ /bin/sh -c " \ # <- Запускаем скрипт сборки ARCH=$(ARCH) \ # (Проверяет на наличие OS=$(OS) \ # аргументов, устанавливает VERSION=$(VERSION) \ # переменные среды и запускает ./build/build.sh \ # `go install`) " @if ! cmp -s .go/$(OUTBIN) $(OUTBIN); then \ # <- Если двоичные данные изменились mv .go/$(OUTBIN) $(OUTBIN); \ # перемещаем их из `.go` в `bin` date >$@; \ fi
make test — тестовая — она снова использует почти тот же docker run, только здесь ещё есть скрипт test.sh (покажем только то, что нас интересует): TARGETS=$(for d in "$@"; do echo ./$d/...; done) go test -installsuffix "static" ${TARGETS} 2>&1 ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) ERRS=$(go vet ${TARGETS} 2>&1 || true)
Эти строчки — важная часть файла. Первая из них собирает тестовые цели, где в качестве параметра указан путь. Вторая строчка запускает тесты и выводит документацию по тестированию ПО. Оставшиеся две строчки запускают gofmt и go vet. Они собирают и выводят ошибки (если таковые имеются).
make container — и, наконец, важнейшая часть — создание развёртываемого контейнера:
.container-$(DOTFILE_IMAGE): bin/$(OS)_$(ARCH)/$(BIN) in.Dockerfile @sed \ -e 's|{ARG_BIN}|$(BIN)|g' \ -e 's|{ARG_ARCH}|$(ARCH)|g' \ -e 's|{ARG_OS}|$(OS)|g' \ -e 's|{ARG_FROM}|$(BASEIMAGE)|g' \ in.Dockerfile > .dockerfile-$(OS)_$(ARCH) @docker build -t $(IMAGE):$(TAG) -t $(IMAGE):latest -f .dockerfile-$(OS)_$(ARCH) . @docker images -q $(IMAGE):$(TAG) > $@
Код для этой цели довольно прост: сначала он подставляет переменные в in.Dockerfile, а затем запускает docker build для получения образа с «изменёнными» и «последними» тегами. И дальше передаёт имя контейнера на стандартный вывод.
Теперь, когда у нас есть образ, нужно где-то его хранить. С этим нам помогает make push, который помещает образ в хранилище образов Docker registry. make ci — ещё один способ использовать Makefile — задействовать его в процессах непрерывной интеграции и развёртывания приложений (об этом речь пойдёт далее). Эта цель очень похожа на make test: тоже запускает все тесты и плюс к этому генерирует отчёты о покрытии, которые потом используются как вводная информация при проведении анализа кода. make clean — и, наконец, если нам нужно провести очистку проекта, запускаем make clean, который удалит все файлы, сгенерированные предыдущими целями.
Остальные цели можно объединить в две группы: первые не так важны для нормального рабочего процесса, а вторые являются лишь частью других целей, поэтому о них можно не упоминать. Интеграция и развёртывание ПО для идеальной разработки
Завершаем статью важной частью — процессом непрерывной интеграции и развёртывания приложений. Не буду подробно расписывать, что в нем такого — вы и сами прекрасно сможете разобраться (практически в каждой строке есть комментарий, так что всё должно быть понятно):
# Matrix build запускает 4 параллельные сборки matrix: include: - language: go # Сборка и тестирование sudo: required services: - docker script: - export GO111MODULE=on - go mod vendor # Загружаем зависимости - make build # Собираем приложение - test -f bin/linux_amd64/blueprint # Тест на наличие двоичных данных, полученных на предыдущем этапе - make all-container # Создаём все докерные контейнеры - docker images | grep "^docker.pkg.github.com/martinheinz/go-project-blueprint /blueprint.*__linux_amd64" # Проверяем наличие созданных образов - make test # Запускает тесты внутри тестового образа - language: go # SonarCloud addons: sonarcloud: organization: martinheinz-github token: secure: "tYsUxue9kLZWb+Y8kwU28j2sa0pq20z2ZvZrbKCN7Sw0WGtODQLaK9tZ94u1Sy02qL5QcabukbENmbvfouzXf4EfaKjDmYH9+Ja22X26MfTLVpaCDTQEGmNyREOFCHpjNXPgDMv1C70By5U+aPWSYF/lehB5rFijwCf7rmTFRNUDeotCTCuWb2dIkrX2i6raVu34SvqqGxKQmmH+NPLe7uKO/wXqH+cWQH1P9oJYeVksNGruw4M0MznUeQHeJQYpTLooxhEEzYiBbkerWGDMwBdZdPQwVrO2b8FEDRw/GWTFoL+FkdVMl4n4lrbO/cQLbPMTGcfupNCuVHh1n8cGp8spMkrfQGtKqvDRuz2tBs0n1PWXCRS6pgZQw/ClLPgi/vVryVRwOabIHSQQLRVhcdp8pkYdyX3aH1EdlIHiJLT6sacS0vJPqZMF/HNsPEoHe4YdiYvx/tcYMU63KQVZzgF4HfQMWy69s1d0RZUqd+wrtHU1DHwnkq1TSe+8nMlbvbmMsm6FVqGistrnVjx4C9TjDWQcjprYU40zCvc1uvoSPimVcaD8ITalCDHlEfoV7wZuisV8+gJzOh9pDZ/joohW7/P3zklGgI2sH7qt62GE4o5UyRArzJC7eIj7Oxx6GdbeEqw09M4rCfR1g5tHWIqVHz5CajvkXkPqrRGu2oI=" before_script: - ./reports.sh # Создаёт каталоги и файлы для отчётов - export GO111MODULE=on - go mod vendor # Загружаем зависимости - make ci # Запускаем тесты и генерируем отчёты (см. этап `ci` в Makefile) script: - sonar-scanner # Запускаем анализ с помощью плагина SonarCloud scanner plugin - language: go # CodeClimate before_script: - ./reports.sh # Создаём каталоги и файлы для отчётов - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter # Загружаем генератор тестовых отчётов CodeClimate - chmod +x ./cc-test-reporter # Делаем его исполняемым - ./cc-test-reporter before-build # Уведомляем CodeClimate о готовом отчёте script:
- export GO111MODULE=on - go mod vendor # Загружаем зависимости - make ci # Запускаем тесты и генерируем отчёты (см. этап `ci` в Makefile)
after_script: - ./cc-test-reporter after-build -t gocov --exit-code $TRAVIS_TEST_RESULT # Отправляем отчёт в CodeClimate или уведомляем о неудавшейся сборке кодом завершения - language: go # Помещаем контейнер в хранилище services: - docker if: branch = master script: - export GO111MODULE=on - go mod vendor # Загружаем зависимости - echo "$DOCKER_PASSWORD" | docker login docker.pkg.github.com -u "$DOCKER_USERNAME" --password-stdin # Подключаемся ко хранилищу GitHub Registry, используя переменные среды Travis - make container # Создаём изменённые и последние образы - make push # Помещаем образ в хранилище notifications: email: false
Но кое-что можно прояснить. В этой сборке Travis использована сборка Matrix Build с 4 параллельными заданиями для ускорения всего процесса:
Сборка и тестирование: здесь мы проверяем, что приложение работает как надо; SonarCloud: здесь мы генерируем отчёты о покрытии и отправляем их на сервер SonarCloud; CodeClimate: здесь — как и в предыдущем задании — мы генерируем отчёты о покрытии и отправляем их, только на этот раз в CodeClimate с помощью их генератора тестовых отчётов; Push to Registry: и, наконец, помещаем наш контейнер в хранилище GitHub Registry.
Заключение
Надеюсь, эта статья поможет вам в ваших будущих разработках кода на Go. Все подробности изложены в репозитории.
В следующей части узнаем, как на базе этого макета проекта, который мы сегодня выстроили, с лёгкостью создавать интерфейсы RESTful API, тестировать с базой данных в памяти, а также настраивать крутую документацию (а пока можно подсмотреть в ветке rest-api репозитория). 🙂
Перевод статьи Martin Heinz: Ultimate Setup for Your Next Golang Project (впервые опубликована на martinheinz.dev). Войдите, чтобы отправлять и оценивать комментарии Расскажите, что вы об этом думаете? Магические методы в Python 7 марта 2020 Создание объекта dict, принимающего только целые и плавающие числа в качестве значений
В этом сценарии мы реализуем класс, который создает объекты-словари, принимающие только целые и плавающие значения.
При добавлении других типов данных, таких как строки, списки и кортежи, будет появляться исключение, указывающее пользователю, что пользовательский объект dict может принимать только целые и плавающие числа в качестве значений.
Для реализации этого процесса используются следующие методы:
__int__, __setitem__ и __str __
Для начала создаем пользовательский класс CustomIntFloat и передаем dict в список наследования аргументов. Это означает, что созданный объект будет вести себя как словарь, за исключением тех мест, в которых это поведение будет выборочно изменено.
Затем создаем метод __init__, чтобы сконструировать объект dict CustomIntFloat, который принимает ключ и значение в список аргументов, установленный в тип None по умолчанию. Таким образом, если пользователь создает объект класса CustomIntFloat без передачи ключа или значения, будет сгенерирован пустой dict. Данное условие гласит: если ключ не передан, то параметру ключа по умолчанию присваивается аргумент None, а пустой dict создается путем ссылки на объект CustomIntFloat с атрибутом класса empty_dict.
Если пользователь указывает ключ length и соответствующее значение, которое является экземпляром класса int или float, то ключ и значение будут установлены в объекте.
Наконец, если пользователь указывает несколько ключей и значений в качестве итерируемых в операторе else, то они будут заархивированы функцией zip и им будет присвоено имя переменной zipped. Выполняем цикл на zipped, чтобы проверить, имеет ли значение тип int или float. Если нет, то будет сгенерировано пользовательское исключение CustomIntFloatError. Класс исключения CustomIntFloatError и метод __str__
При генерации исключения CustomIntFloatError создается экземпляр класса CustomIntFloatError.
Таким образом, этот пользовательский класс исключений нуждается в помощи magic-методов __init__ и __str__. Созданный экземпляр принимает переданное значение и устанавливает его в качестве значения атрибута в классе CustomIntFloatError.
Это означает, что при появлении сообщения об ошибке значение, переданное в __init__ объекта CustomIntFloat, может быть установлено как атрибут (self.value) в классе CustomIntFloatError и с легкостью проверено.
Если ввод неверный, то появляется исключение CustomIntFloatError, а объект не создается. Сообщение об ошибке информирует пользователя о том, что допустимыми являются только целые и плавающие значения.
Аналогичным образом при попытке создать экземпляр объекта z (который был закомментирован) с несколькими ключами и значениями, возникает то же исключение, информирующее пользователя о том, что ‘three’ не является допустимым вводом.
# z = CustomIntFloat(key=['a', 'b', 'c'], value=[1, 2, 'three']) z = CustomIntFloat(key=['a', 'b', 'c'], value=[1, 2, 3])
Метод __setitem__ __setitem__ — это magic-метод, который вызывается при установке ключа и значения в словаре. Если после создания объекта CustomIntFloat пользователь попытается добавить значение, которое не относится к типу int или float, появится то же исключение CustomIntFloatError. Ниже показано, как установить ключ и значение:
x = CustomIntFloat('a', 1) print(type(x)) x['b'] = 2.1 print(x)
- x['c'] = 'Three'
В результате недопустимого ввода возникает исключение CustomIntFloatError:
Исходный код:
class CustomIntFloatError(Exception): def __init__(self, value): self.value = value
def __str__(self): return self.value + ' is not valid\nOnly Integers and floats are valid values \nin CustomIntFloat(dict) ' class CustomIntFloat(dict):
empty_dict = {}
def __init__(self, key=None, value=None): if key is None: CustomIntFloat.empty_dict = {}
elif len(key) == 1 and isinstance(value, (int, float)): dict.__setitem__(self, key, value) else: zipped = zip(key, value) for tup in zipped: if isinstance(tup[1], (int, float)): dict.__setitem__(self, tup[0], tup[1]) else: raise CustomIntFloatError(tup[1])
def __setitem__(self, key, value): if not isinstance(value, (int, float)): raise CustomIntFloatError(value) return dict.__setitem__(self, key, value)
Обзор класса CustomIntFloat
С помощью наследования через такие встроенные классы, как dict, можно настраивать поведение через повторную реализацию magic-методов. У этого подхода есть множество преимуществ.
Стоит отметить, что пользователю не нужно изучать новый синтаксис. Он может добавить ключ и значение к объекту dict CustomIntFloat привычным образом. Единственным отличием является выбор допустимых значений типа int и float. Если пользователь указывает любой другой тип, то сообщение об ошибке информирует его об этом и указывает допустимые типы значений. Сразу несколько методов Примеры с использованием математических операторов
__sub__, __add__ и __mul__ (с пользовательским __repr__)
С помощью magic-методов можно также воспользоваться математическими операторами в Python. Рассмотрим на примере таких методов, как __add__, __sub__ и __mul__ в созданном нами пользовательском объекте.
Такие операторы, как +, -, / и *, являются полиморфными методами. Как показано ниже, знак плюса (+) является полиморфным и может использоваться для объединения строк, суммирования целых чисел и комбинирования списков. Это возможно благодаря тому, что такие типы, как str, list и int, обладают методом add в соответствующих классах. Python просто преобразует знак + в вызов метода __add__ для объекта, который его вызвал (см. примеры ниже).
Это означает, что при включении метода __add__ в класс можно воспользоваться знаком + в объектах. Применение magic-методов оператора в классе
Создаем класс NumOperations, который генерирует объекты NumOperations. Когда пользователь этого класса передает список в список аргументов __init__, он устанавливается в качестве атрибута в объекте NumOperations и получает название .math_list.
После создания объекта(ов) NumOperations можно с легкостью использовать magic-методы для работы с ними и передачи математической операции.
Например, magic-метод __sub__ принимает 2 объекта NumOperations, объединяет их списки и просматривает другие соответствующие им списки кортежей. Второй элемент в кортеже вычитается из первого, и это значение добавляется в новый список minuslst и передается в качестве аргумента в конструктор NumOperations.
Теперь он возвращает новый объект NumOperations.
Эта передача выполняется по методу __sub__. Это означает, что можно воспользоваться оператором минус (-).
Magic-метод __repr__ реализуется повторно, чтобы возвращать представление строки списка, установленное в новом объекте. Он был изменен, поэтому когда пользователь печатает выходные данные двух объектов NumOperations, результат будет соответствовать ожиданиям.
Ниже представлен список, где элементы были вычтены друг из друга:
[90, 81, 72, 63, 54].
Методы __add__ и __mul__ реализуются аналогично __sub__, однако используют списковое включение для сокращения количества строк кода. Magic-методы вычитания, сложения и умножения были определены для работы с пользовательскими объектами NumOperation.
Такое поведение при передаче аналогично таким пакетам анализа данных, как Pandas и Numpy.
Методы __add__ и __mul__ также предназначены для работы с двумя объектами NumOperations. Это означает, что пользователь может воспользоваться оператором плюс + и умножением *. Как видно из приведенного ниже примера, q является результатом x * y, который возвращает новый объект NumOperations. При вводе q мы получаем представление строки операции передачи в виде списка.
Исходный код доступен по ссылке на GitHub gist:
class NumOperations(object): def __init__(self, math_list): self.math_list = math_list def __sub__(self, other): minuslst = [] zipped = zip(self.math_list, other.math_list) for tup in zipped: minuslst.append(tup[0] - tup[1]) return NumOperations(minuslst)
def __add__(self, other): addlst = [x + y for x, y in zip(self.math_list, other.math_list)] return NumOperations(addlst)
def __mul__(self, other): mullst = [x * y for x, y in zip(self.math_list, other.math_list)] return NumOperations(mullst)
def __repr__(self): return str(self.math_list)
x = NumOperations([100, 90, 80, 70, 60]) y = NumOperations([10, 9, 8, 7, 6]) p = x - y z = x + y q = x * y print('Subtraction: ' + str(p)) print('Addition: ' + str(z)) print('Multiplication: ' + str(q))
Читайте также:
Анализ аудиоданных с помощью глубокого обучения и Python (часть 1) Максимальная производительность Pandas Python Продвинутые методы и техники списков в Python
Перевод статьи Stephen Fordham: Using Magic Methods in Python Войдите, чтобы отправлять и оценивать комментарии Расскажите, что вы об этом думаете?
ИСТОЧНИК: