2017-08-06

О Makefile

Вы делали когда-нибудь «заклинание» ./configure && make && make install? Им собирается 80% юниксовых/линуксовых программ из исходников. configure — это Autotools, про них я не буду рассказывать. make — это GNU make.
Learn Makefile
Оригинальный make появился, если верить Википедии, в 1977 году, в недрах Bell Labs. Сейчас имеется два мейка: BSD make и GNU make. Чем они отличаются — это отдельный религиозный вопрос. Но если у вас Linux, у вас по-любому будет GNU make.
Make в качестве параметра принимает имя некоторого правила. А правила задаются в файле Makefile в текущем каталоге.
Зачем явисту может понадобится make и Makefile? Ведь у нас есть великолепнейшие системы сборки: Ant (кто-нибудь им ещё пользуется?), Maven, Gradle. Они действительно могут многое: и собрать, и прогнать тесты, и запустить (сервер локально), и задеплоить (в удалённый контейнер приложений/сервлетов или собранный артефакт в репозиторий). Но, вы всегда помните, что именно надо написать после mvn, чтобы пересобрать супер-jar, задеплоить его в Artifactory, и при этом вам зачем-то хочется пропустить запуск тестов?
А ведь ещё есть более посторонние штуки. Как запустить Docker контейнеры вашей системы локально? docker-compose up или лучше docker-compose up -d? А в каком каталоге? А надо ли перед этим ещё что-то собрать?
А вы деплоите с помощью Ansible? А вы используете роли? А в каком каталоге проекта это всё у вас лежит? А у вас отдельные inventory файлы для разных окружений? Вариантов может быть очень много, а от этого зависит команда, которую нужно выполнять для деплоя. У нас на проектах получается что-то вроде: cd ansible && ansible-playbook -i production deploy.yml. И это далеко не самый каноничный вариант.
Выходов из необходимости запоминать длинные, но необходимые команды есть несколько.
Самые ленивые команды оставляют всё как есть. В результате новичку приходится надоедать старичкам в попытке вытянуть крупицы тайных знаний о том, как собирать и работать с проектом.
Самые упорные команды пишут документацию. Да, в каком-нибудь README.md рядышком, или в вики проекта, или, упаси боже, в корпоративной вики. Описывают всё. Что куда как пойти, и какие команды выполнить. Новичку приходится долго читать, тщательно копировать команды. И молиться, что документация не устарела, а команды не содержат ошибок.
Самые продвинутые команды, которые работают в Linux и слышали, что такое shell script... Хотя ладно, батники в Windows тоже никто не отменял... Они пишут эти самые скрипты. Помещая туда самые сложные команды или последовательности команд. Беда в том, что ворох скриптов не добавляет понимания, даже наоборот. И скрипты приходится документировать.
GNU Make O'Reilly book
Maven и Gradle всегда имеют свой жизненный цикл. И вы лишь можете выбрать, какой вариант этого цикла и с какого места запустить. Дополнительные плугины, особенно в Gradle, изрядно усложняют и дополняют этот жизненный цикл. Количество возможных «слов», которые можно приписать после mvn или gradlew, просто чудовищно.
Ant работал по-другому. Там надо было явно прописывать targetы, со своими, нежно выдуманными, именами, и зависимостями между ними. И вот эти таргеты и выполнялись.
В этом смысле make подобен antу. Можно считать, что в Makefile вы пишите отдельные именованные правила сборки, тоже с зависимостями между ними, а потом запускаете. Только в make одним шагом сборки может быть любая команда или последовательность команд, доступных для выполнения в данной ОС, а не команды ограниченного набора плугинов. Make работает на том же уровне, что shell скрипты. Он может объединить систему сборки с инструментами деплоя и всем прочим.
all: build

build: jar docker

jar:
    mvn package

docker:
    docker-compose build

run:
    docker-compose up

clean:
    mvn clean
    docker-compose down
Синтаксис объявления правила такой.
имя-правила: зависимость-1 зависимость-2
    команда-1
    команда-2
Тут ахтунг. Makefile — это единственный известный мне синтаксис, чувствительный к табуляции. Команды в правиле выделены не просто отступом, а именно что знаком табуляции. Который ASCII 0x09, "\t", , U+0009.
Большинство программистских редакторов уже давным давно при нажатии кнопки Tab ↹ делают именно отступ, а не вставляют табуляцию. Поэтому .editorconfig вам в помощь.
[Makefile]
indent_style = tab
На самом деле, в терминах самого make, это нифига не имя правила, зависимости и команды. Это цели, пререквизиты и рецепты.
targets : prerequisites ; recipe
    recipe
    …
Цель — это не абстрактное имя, выбранное вами, а имя файла, который должен появиться в результате выполнения рецепта. Целевой файл правила.
Пререквизиты — это тоже имена (или маски) файлов, которые при этом должны быть новее (смотрится дата последней модификации), чем цель. И эти файлы нужны рецепту, чтобы создать цель.
Рецепт — это рецепт. Команды.
Всё это создавалось для компиляции программ на C. Тут принято, что результат компиляции, это файл с тем же именем в том же каталоге, но с другим расширением.
foo.o : foo.c defs.h
    cc -c -g foo.c
Более того, много правил именно для C уже неявно присутствуют в GNU make. В Makefile достаточно лишь указать название итоговой программы, и из каких модулей она должна быть собрана.
При большом желании можно повторить подобное и для Явы. Ведь Maven, в отличие от Gradle, не умеет не пересобирать проект, когда ничего не менялось. Придётся только немного извратиться, ибо в мире Ява принято рассовывать исходники и результат компиляции в разные каталоги, и к тому же сами исходники щедро раскиданы по развесистому дереву каталогов.
# правило сборки jar: для jar нужен jar в target
jar: main-project/target/*.jar

# http://stackoverflow.com/questions/4036191/sources-from-subdirectories-in-makefile
# Make does not offer a recursive wildcard function, so here's one:
rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2))

# определения длинных списков файлов
ALL_MAIN_JAVA := $(call rwildcard,main-project/src/,*.java)
ALL_MAIN_RESOURCES := $(call rwildcard,main-project/src/main/resources/,*)
ALL_LIB_JAVA := $(call rwildcard,lib-project/src/,*.java)
ALL_LIB_RESOURCES := $(call rwildcard,lib-project/src/main/resources/,*)

# правило сборки jar, ему нужны все эти файлы
main-project/target/*.jar: $(ALL_MAIN_JAVA) $(ALL_MAIN_RESOURCES) $(ALL_LIB_JAVA) $(ALL_LIB_RESOURCES)
        mvn package
Целями и пререквизитами могут быть маски файлов. А в данном случае с помощью грязного хака получается маска с рекурсивным поиском по подкаталогам.
Так как цели — это файлы, но некоторые цели — выдуманные имена, то может случиться конфуз, если файл или каталог с именем «jar» или «clean», или, что уже вполне вероятно, «build», окажется в текущем каталоге. Make тогда может посчитать, что сборка уже имела место, и не выполнит рецепт.
Чтобы этого избежать, цели с выдуманными именами надо явно помечать как «.PHONY».
.PHONY: jar
jar:
    mvn package

.PHONY: clean
clean:
    mvn clean
    docker-compose down
.PHONY — это тоже цель, но по специальным искусственным значением.
Как вы уже наверное догадались, если запустить make без параметров, то будет выполнена первая цель.
В Makefile можно определять переменные. А можно и переопределять переменные. Можно определить дефолтные значения в самом Makefile, а переопределить в командной строке или через переменные окружения.
VAR ?= makefile variable

print:
    echo $(VAR)
$ make
echo makefile variable
makefile variable
$ make VAR="overridden variable" print
echo overridden variable
overridden variable
$ VAR="environment variable" make print
echo environment variable
environment variable
Заметьте, что значения переменных подставляются в текст рецепта как есть, и лишь потом рецепт выполняется. В то же время рецепт, если является командой оболочки, может определять и использовать свои переменные. Нужно только правильно экранировать «$», удваивая его.
Рецепты (каждый сам по себе, если их несколько) выполняются в отдельном подпроцессе. Причём это вовсе не обязательно будет процесс оболочки. Make может просто запустить указанную команду, т.е. сам работает оболочкой. Так что если вам нужен именно bash или zsh для выполнения команды, об этом надо специально позаботиться.
shell:
    echo 1-$$$$-$$0
    sh -c "echo 2-$$$$-$$0"
$ make
echo 1-$$-$0
1-15199-/bin/sh
sh -c "echo 2-$$-$0"
2-15200-/bin/sh
Соответственно, если вы делаете cd куда-то && что-то сделать, текущий каталог меняется только для этой команды, что очень удобно. А если в этом другом каталоге есть свой Makefile, то можно выполнить его правила. Это тоже очень удобно, иметь по Makefile в каждом подпроекте, со своими правилами сборки. А на «глобальном» уровне объединять сборки компонентов под одним правилом и делать другие глобальные вещи.
build:
    cd sub1 && $(MAKE) build
    cd sub2 && $(MAKE) build
$ make
cd sub1 && make build
make[1]: Entering directory '.../dirs/sub1'
echo building sub1
building sub1
make[1]: Leaving directory '.../dirs/sub1'
cd sub2 && make build
make[1]: Entering directory '.../dirs/sub2'
echo building sub2
building sub2
make[1]: Leaving directory '.../dirs/sub2'
Здесь MAKE — это предопределённая переменная, которая содержит полный путь до текущего make. Это чтобы не зависеть от всяких путей типа /usr/bin/make.
Makefile — это не только про сборку. Например, вместо длинных объяснений и тыкания в ссылки официальной документации, можно просто дать Makefile, в котором всё есть.
Вот, например, как поставить Docker и Docker Compose на Red Hat Enterprise Linux?
DOCKER_VERSION=17.03.2.ce
DOCKER_COMPOSE_VERSION=1.11.2

.PHONY: help
help:
    @echo "make install - to install Docker and other necessary components"
    @echo "make pull    - to download and update necessary Docker images"
    @echo "..."

# docker install targets

.PHONY: install
install: add_docker_repo install_docker start_docker install_docker_compose

.PHONY: add_docker_repo
add_docker_repo:
    sudo yum install -y yum-utils
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    sudo yum -y makecache fast

.PHONY: enable_extras_repo
enable_extras_repo:         # it's necessary to install container-selinux which is required by Docker
    sudo yum-config-manager --enable rhel-7-server-extras-rpms
    sudo yum -y makecache fast

.PHONY: install_docker
install_docker:
    sudo yum -y --setopt=obsoletes=0 install docker-ce-$(DOCKER_VERSION)

.PHONY: start_docker
start_docker:
    sudo systemctl enable docker
    sudo systemctl start docker

.PHONY: install_docker_compose
install_docker_compose:
    sudo curl -L https://github.com/docker/compose/releases/download/$(DOCKER_COMPOSE_VERSION)/docker-compose-Linux-x86_64 -o /usr/bin/docker-compose
    sudo chmod +x /usr/bin/docker-compose
Есть в Makefile и условия, и циклы. Читайте документацию и разбирайтесь.
Старайтесь лишь не переусложнять Makefile, который вы пишите руками (Autotools генерируют Makefile, в результате он пугает). Оставьте только самые важные и часто используемые команды, чтобы было удобно.

2017-07-22

О java.time

Ну наконец-то, аж в восьмой яве, появилось отличное и правильное API для работы со временем. Теперь можно смело выкинуть java.util.Date и java.text.DateFormat. Теперь у нас есть java.time.
Java Time!
Чаще всего вам понадобится просто отметка времени, точка на временной оси. Это — java.time.Instant.
Можно получить момент времени сейчас. Можно получить момент времени из юниксовых секунд от начала эпохи. Можно получить момент времени из миллисекунд от начала эпохи, как это представляется в java.util.Date. А можно взять момент времени из строки в формате ISO 8601 с буковкой «Z» в конце, что означает, что это время в UTC, а значит, не подвержено особенностям разных таймзон.
>>> import java.time.Instant
>>>
>>> Instant.now()
2017-07-22T08:39:59.665Z
>>> Instant.ofEpochSecond(1_500_000_000)
2017-07-14T02:40:00Z
>>> Instant.ofEpochMilli(1_500_000_000_000)
2017-07-14T02:40:00Z
>>> Instant.parse("2017-07-14T02:40:00Z")
2017-07-14T02:40:00Z
Соответственно, можно и наоборот, извлечь из Instant число секунд или миллисекунд, или преобразовать в строку.
>>> val instant = Instant.now()
>>>
>>> instant.getEpochSecond()
1500713246
>>> instant.toEpochMilli()
1500713246133
>>> instant.toString()
2017-07-22T08:47:26.133Z
Эта возможность гонять из чисел и строк туда и обратно очень полезна. Сохраняйте таким образом Instant куда угодно и загружайте обратно, это будет совершенно правильно и безопасно.
Допустим, у нас есть какая-то дата вида «12/07/2017» (тут главное, сразу выяснить, где месяц, а где день месяца). Её можно распарсить с помощью java.time.format.DateTimeFormatter и получить java.time.LocalDate.
>>> import java.time.format.DateTimeFormatter
>>> import java.time.LocalDate
>>>
>>> val dateString = "12/07/2017"
>>> val dateFormat = DateTimeFormatter.ofPattern("dd/MM/yyyy")
>>> val localDate = LocalDate.parse(dateString, dateFormat)
>>> localDate
2017-07-12
LocalDate содержит сведения о годе, месяце и дне (конкретного календаря). Не больше, но и не меньше. Он ничего не знает ни о таймзоне, ни о времени вообще. Этого недостаточно, чтобы определить точку на временной оси. Но вполне достаточно, чтобы обозначить день в календаре.
>>> Instant.from(localDate)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: 2017-07-12 of type java.time.LocalDate
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
Допустим, у нас есть какое-то время вида «08:52:17» (тут надо сразу выяснить, что это действительно 24-часовой формат, иначе появляется неоднозначность). Его можно распарсить, снова с помощью DateTimeFormatter, и получить java.time.LocalTime.
>>> import java.time.LocalTime
>>>
>>> val timeString = "08:52:17"
>>> val timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss")
>>> val localTime = LocalTime.parse(timeString, timeFormat)
>>> localTime
08:52:17
LocalTime содержит сведения о часах, минутах, секундах (и наносекундах). Какого-то неопределённого дня. В каком-то неопределённом часовом поясе. Очевидно, этого тоже недостаточно, чтобы определить конкретную точку на временной оси. Но достаточно, например, чтобы поставить будильник, на определённое локальное время, каждый день.
>>> Instant.from(localTime)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: 08:52:17 of type java.time.LocalTime
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
DateTimeFormatter похож на java.text.SimpleDateFormat. Да, он поддерживает такой же синтаксис шаблонов. Но его ещё можно собрать с помощью DateTimeFormatterBuilder, накидав нужные поля в нужном порядке. Ещё в нём есть куча предопределённых форматтеров вроде DateTimeFormatter.ISO_INSTANT. А ещё DateTimeFormatter, в отличие от SimpleDateFormat, потокобезопасен (ибо иммутабелен).
Итак, у нас есть LocalDate и LocalTime. Что с ними можно сделать? Можно соединить их вместе, чтобы получить определённое время определённого дня. Это будет java.time.LocalDateTime.
>>> import java.time.LocalDateTime
>>>
>>> val localDateTime = LocalDateTime.of(localDate, localTime)
>>> localDateTime
2017-07-12T08:52:17
Этого всё ещё недостаточно, чтобы получить конкретный момент времени, потому что неизвестно, где именно на планете Земля это событие происходит.
>>> Instant.from(localDateTime)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: 2017-07-12T08:52:17 of type java.time.LocalDateTime
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
Нам нужна таймзона.
Допустим, известно, что дело происходит в Мексике, где таймзона, в базе данных IANA, носит название «Mexico/General» (хотя правильнее, вообще-то, «America/Mexico_City»). По названию таймзоны получаем java.time.ZoneId, и, добавив его к LocalDateTime, получаем java.time.ZonedDateTime.
>>> import java.time.ZoneId
>>> import java.time.ZonedDateTime
>>>
>>> val timeZone = ZoneId.of("Mexico/General")
>>> val zonedDateTime = ZonedDateTime.of(localDateTime, timeZone)
>>> zonedDateTime
2017-07-12T08:52:17-05:00[Mexico/General]
Кроме ZoneId, который представляет собой таймзону в конкретной географической области, включая переходы на летнее и зимнее время, а также исторические события типа передвижения границ или смены часовых поясов, можно ещё просто задать смещение в виде java.time.ZoneOffset. Отсюда можно получить либо тот же (но немножко другой) ZonedDateTime, либо java.time.OffsetDateTime.
>>> import java.time.ZoneOffset
>>> import java.time.OffsetDateTime
>>>
>>> val zoneOffset = ZoneOffset.ofHours(-5)
>>> ZonedDateTime.of(localDateTime, zoneOffset)
2017-07-12T08:52:17-05:00
>>> val offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset)
>>> offsetDateTime
2017-07-12T08:52:17-05:00
И ZonedDateTime, и OffsetDateTime, и Instant представляют конкретную точку на оси времени.
>>> Instant.from(zonedDateTime)
2017-07-12T13:52:17Z
>>> Instant.from(offsetDateTime)
2017-07-12T13:52:17Z
>>>
>>> ZonedDateTime.from(Instant.now())
java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: 2017-07-22T10:49:38.075Z of type java.time.Instant
Caused by: java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: 2017-07-22T10:49:38.075Z of type java.time.Instant
>>> ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
2017-07-22T16:51:07.317+06:00[Asia/Omsk]
>>> OffsetDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
2017-07-22T16:51:49.311+06:00
Разница будет в том, что с ними можно сделать.
Instant — это лишь точка, и больше ничего. Её можно сдвинуть в прошлое или будущее, на секунды или миллисекунды, даже на часы. Но будет ли через два часа уже завтра в конкретном Мехико-сити или Омске, Instant вам не ответит.
>>> instant
2017-07-22T08:47:26.133Z
>>> instant.plusSeconds(4)
2017-07-22T08:47:30.133Z
>>> instant.plusMillis(866)
2017-07-22T08:47:26.999Z
>>>
>>> import java.time.Duration
>>> instant.plus(Duration.ofHours(4))
2017-07-22T12:47:26.133Z
>>> instant + Duration.ofHours(4)
2017-07-22T12:47:26.133Z
OffsetDateTime — это точка, которая знает своё смещение от UTC. Но она ничего не знает о летнем и зимнем времени. Зимой в Мексике зимнее время, UTC-6, но OffsetDateTime будет считать по-прежнему в UTC-5.
>>> offsetDateTime
2017-07-12T08:52:17-05:00
>>>
>>> import java.time.Period
>>> offsetDateTime + Period.ofMonths(6)
2018-01-12T08:52:17-05:00
А вот ZonedDateTime — это точка, которая знает о своём местоположении всё (если, конечно, была создана с ZoneId, если создать с ZoneOffset, то поведение не будет отличаться от OffsetDateTime). Включая то, каким было смещение от UTC в данной местности при царе Горохе. (Кто бы знал, что такое UTC-06:36?)
>>> zonedDateTime
2017-07-12T08:52:17-05:00[Mexico/General]
>>> zonedDateTime + Period.ofMonths(6)
2018-01-12T08:52:17-06:00[Mexico/General]
>>> zonedDateTime - Period.ofYears(100)
1917-07-12T08:52:17-06:36:36[Mexico/General]
Если вам нужно просто обозначить точку на оси времени, используйте Instant. Если вам нужно манипулировать временем по всем правилам календаря, используйте ZonedDateTime с правильным ZoneId.
Когда у нас последний день текущего месяца? Тут, кстати, хватит LocalDate.
>>> LocalDate.now()
2017-07-22
>>> LocalDate.now() + Period.ofMonths(1)
2017-08-22
>>> LocalDate.now().plus(Period.ofMonths(1))
2017-08-22
>>> LocalDate.now().plus(Period.ofMonths(1)).withDayOfMonth(1)
2017-08-01
>>> LocalDate.now().plus(Period.ofMonths(1)).withDayOfMonth(1).minus(Period.ofDays(1))
2017-07-31
На самом деле все эти Instant, LocalDate, LocalTime, LocalDateTime и ZonedDateTime реализуют интерфейс TemporalAccessor. Именно объекты этого интерфейса возвращает DateTimeFormatter.parse() и принимают методы типа Instant.from(). TemporalAccessor позволяет узнать, какие TemporalField (год, месяц, день, часы, минуты и т.п.) имеются в данном объекте и запросить их значения.
>>> import java.time.temporal.ChronoField
>>>
>>> LocalTime.now().isSupported(ChronoField.HOUR_OF_DAY)
true
>>> LocalTime.now().isSupported(ChronoField.DAY_OF_MONTH)
false
>>> LocalTime.now().get(ChronoField.HOUR_OF_DAY)
17
>>> LocalTime.now().get(ChronoField.DAY_OF_MONTH)
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
А Duration и Period реализуют интерфейс TemporalAmount. Тут всё просто. Duration умеет дни (24 часа ровно), часы, минуты, секунды, миллисекунды, наносекунды. Period умеет года, месяцы, недели, дни. Они задают количество TemporalUnit, которые можно прибавить или отнять к/от TemporalAccessor. Можно задавать длительность в виде другой половины формата ISO 8601.
>>> LocalTime.now()
17:38:14.342
>>> LocalTime.now() + Duration.parse("PT1H30M")
19:08:17.343
>>> LocalDate.now()
2017-07-22
>>> LocalDate.now() + Period.parse("P1M8D")
2017-08-30
Получается, что TemporalAccessor хранят значения времени, TemporalAmount позволяют сдвигать значения времени. А ещё есть TemporalAdjuster, которые позволяют подкручивать время более хитрыми способами. В простейшем случае можно просто выставить какое-то TemporalField в нужное значение. В более интересных случаях можно поискать тот же конец месяца или следующий понедельник. Для этого используются методы with*.
>>> import java.time.Year
>>>
>>> LocalDate.now()
2017-07-22
>>> LocalDate.now().with(Year.of(2019))
2019-07-22
>>>
>>> import java.time.temporal.TemporalAdjusters
>>>
>>> LocalDate.now().with(TemporalAdjusters.firstDayOfMonth())
2017-07-01
>>> LocalDate.now().with(TemporalAdjusters.lastDayOfMonth())
2017-07-31
>>>
>>> import java.time.DayOfWeek
>>>
>>> LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))
2017-07-24
Всё сложно, но мощно.
Все методы могут кидать исключения. Особенно parse(). Базовый класс исключений тут java.time.DateTimeException. Причём он — наследник java.lang.RuntimeException. А это значит, что компилятор не попросит вас обернуть манипуляции с датой и временем в try-catch. В новейших API в Java явно прослеживается отказ от использования checked exceptions. Будьте внимательны.
А ещё есть java.time.Clock. Он является источником текущего времени (в виде Instant), а также текущей таймзоны и всего такого. Но его можно переопределить, например, для тестов. Например, чтобы время застряло на одной отметке. Если вы спроектируете свои классы так, чтобы текущие настройки и время всегда получались из Clock, то сможете легко тестировать все аспекты поведения, завязанные на время.
>>> import java.time.Clock
>>>
>>> val defaultClock = Clock.systemDefaultZone()
>>> defaultClock.instant()
2017-07-22T13:24:38.378Z
>>> defaultClock.instant()
2017-07-22T13:24:40.727Z
>>> defaultClock.instant()
2017-07-22T13:24:42.055Z
>>>
>>> val fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
>>> fixedClock.instant()
2017-07-22T13:26:09.282Z
>>> fixedClock.instant()
2017-07-22T13:26:09.282Z
>>> fixedClock.instant()
2017-07-22T13:26:09.282Z
А для особо экзотических случаев у нас есть java.time.chrono.Chronology. Это другие календари. Наслаждайтесь.
>>> import java.time.chrono.IsoChronology
>>> IsoChronology.INSTANCE.dateNow()
2017-07-22
>>> import java.time.chrono.HijrahChronology
>>> HijrahChronology.INSTANCE.dateNow()
Hijrah-umalqura AH 1438-10-28
>>> import java.time.chrono.JapaneseChronology
>>> JapaneseChronology.INSTANCE.dateNow()
Japanese Heisei 29-07-22
>>> import java.time.chrono.MinguoChronology
>>> MinguoChronology.INSTANCE.dateNow()
Minguo ROC 106-07-22
>>> import java.time.chrono.ThaiBuddhistChronology
>>> ThaiBuddhistChronology.INSTANCE.dateNow()
ThaiBuddhist BE 2560-07-22
Обратите внимание, что все хронологии работают только с датой. Видимо, день — это слишком ничтожный промежуток времени, чтобы какой-то другой способ дробления его на части, кроме сложившихся в Европе двадцати четырёх часов, получил бы распространение. IsoChronology — это наш знакомый григорианский календарь.
Kotlin logo
P.S. Примеры в этой статье выполнялись в Kotlin REPL. Именно поэтому метод plus() можно было заменить на оператор +.

2017-07-08

О самокатах

Самокат — это такая доска с колёсиками и рулём. Так мне всегда представлялось. В детстве у меня не было самоката. Я их видел только на картинках в детских книжках. Зато был велосипед. То ли самокат был непрактичным в наших не самых городских условиях. То ли самокаты не завозили в далёкие уголки нашей красной Родины CCCР.
Самокат в детской книжке
Первый самокат в моей жизни появился, когда мы с дочей проходили в супермаркете через отдел игрушек. Доча просто схватила это чудное розовое недоразумение, и поехала на нём. Эта штука была пластмассовой и трёхколёсной, с чудным механизмом поворота двух передних колёс наклоном агрегата. Никто дочу не учил, она сама поехала. Вот вам первое преимущество самоката: он интуитивно понятен, и не требует обучения. Он понятнее велосипеда, да.
Потом доча подросла, и купили ей уже металлический самокат, на двух десятисантиметровых колёсах.
Оказалось, что на самокате очень удобно перемещаться на пару километров во всякие художественные школы и прочие детские занятия. И ребёнок делает это удивительно резво.
Чтобы поспевать за ребёнком, мы купили взрослый самокат, на двадцатисантиметровых литых колёсах. И нормально. Скорость передвижения взрослого сравнялась со скоростью передвижения ребёнка.
Кикборд для малышей
Я решил провести эксперимент. Всё-таки лето. До работы было три с половиной километра. Сейчас работа немного переехала, и стало четыре километра. Но зато по набережной, вроде как с размеченной велодорожкой. Как добираться до работы?
На общественном транспорте. Пять минут до остановки. Дождаться, влезть в автобус, проехать четыре остановки. Минут десять от остановки (далековато, да). Заплатить 22 рубля.
На личном автомобиле. Туда — минут двадцать, потому что через центр города, минимум через три оживлённых перекрёстка. Обратно получается минут за десять, другим путём. Заплатить минимум рублей 10-15 только за бензин. Но есть одна серьёзная засада: парковаться негде. На всех парковках рядом народ играет в сокобан. Ставить машину приходится в странных местах, где нет никакой гарантии, что её не снесёт кто-нибудь при неудачном манёвре.
Решил поиграть в защитника окружающей среды и сторонника ЗОЖ. Поискал другие способы.
Пешком. Вполне себе вариант. Главное, не торопиться, чтобы не натереть мозоли. Одно плохо: сорок минут в один конец. Дважды по сорок минут каждый рабочий день. Жалко.
На велосипеде. Велосипеда нет. И не будет. Потому что его негде хранить. И так весь балкон самокатами занят. Хотя да, велосипед очень быстрый и весьма экологичный транспорт, если в офисе есть душ.
Сравнительная практика показала, что велосипед быстрее самоката. Велосипед проходимее самоката, на нём без особых проблем можно перепрыгивать бордюры, и ему не страшна плитка (о плитке ниже). Но велосипед значительно крупнее и массивнее обычного самоката. Возможно, маленькие складные велосипеды и компактны. Но есть гипотеза, что они лишены достоинств обычных велосипедов.
На самокате. Взрослый самокат уже есть. Несколько раз добрался на нём до работы и обратно. Впечатления интересные.
Во-первых, хочется послать луч ненависти тем градоначальникам, что придумали мостить тротуары (и велодорожки) плиткой. Вы, ребята, покатайтесь по этой плитке на роликах. Вы видели людей на роликах на набережных Омска, где тёплыми летними вечерами гуляют толпы народу и носятся уймы велосипедистов? Правильно, не видели. Потому что их там нет. Потому что по плитке на роликах кататься невозможно. Самокат, даже с двадцатисантиметровыми колёсами, в этом смысле не намного лучше. После нескольких километров по плитке начинают болеть колени, а мне с ними ещё жить.
Самый гадкий вариант плитки
Во-вторых, самокат быстр, раза в два быстрее, чем пешком, лишь в благоприятных условиях. Встречный ветер (а на набережной он почему-то всегда встречный), затяжной подъем, и всё. Можно упираться, и продолжать толкаться, но всё же, в плане скорости, времени и усилий, проще спешиться.
Конечно, кайфово на самокате ехать вниз по спуску. Он просто едет, отталкиваться не надо. Надо даже притормаживать. Но очевидная гадость в том, что где в одну сторону спуск, в другую сторону будет подъём.
Появились и некоторые, не осознаваемые ранее, требования к самокату.
Колёса должны быть надувные, и побольше. Это единственный известный способ хоть как-то победить чёртову плитку. Так называемые амортизаторы, что ставят на некоторые самокаты, на практике бесполезны.
Дека (та площадка, где вы стоите на самокате), должна быть как можно ниже. Ну и фиг с ним, что брюхом будете цепляться за бордюры, если попытаетесь через них перебраться. Не для того самокаты нужны. И перепрыгивать на них бордюры, это уже специальная самокатная акробатика. Зато дека — эта та «ступенька», на которую вы подымаетесь после каждого толчка. А подыматься придётся ой как много. Это основные физические усилия при езде на самокате. Чем ступенька ниже, тем легче.
Взрослый самокат
Итак. Мне нравится катиться на самокате вниз по склону. Мне не нравятся физические усилия, которые приходится прикладывать на самокате при встречном ветре и на подъёме. Как бороться с ветром и подъёмами, сохраняя экологичность? Электрификацией!
На моноколесе. Это такой сегвей, оно тоже самобалансируется и подкатывается под ездока, но с одним колесом и без руля. Способ передвижения Уткоробота, короче.
Уткоробот
Я давненько почитывал статьи и подумывал о моноколесе. Останавливала только невозможность попробовать сию экзотику прежде, чем купить. А тут по весне коллега-гик как раз решился, и обзавёлся таким аппаратом. Как раз ездить на работу. Я попытался попробовать прокатиться. Оказалось, не всё так просто.
Моноколесо требует обучения. Этому счастливому владельцу потребовалась неделя тренировок, чтобы проехать первые десять метров. Но он утверждает, что это всё равно проще, чем научиться ездить на велосипеде. Кто, кстати, помнит, сколько он на велосипеде учился? Для езды на моноколесе нужна защита, хотя бы роллерная. Ибо шанс навернуться и полететь кувырком вполне неиллюзорный.
Зато со всем остальным у моноколеса всё хорошо. Скорость до 25 км/ч и даже выше. Вполне велосипедная проходимость, колесо-то большое. При достаточном навыке тоже можно запрыгивать на бордюры. Масса в 12-15 кг. Запас хода в 25-30 км и больше. Потрясающая компактность: это просто тяжёлый чемоданчик, из которого снизу торчит колесо. Коллега даже в магазин на нём ездит, просто уладывает в супермаркетную тележку.
Моноколесо от Ninebot
В общем, к моноколесу я, пожалуй, ещё как-нибудь вернусь. А пока этот самый коллега говорит: а ведь ещё есть электросамокаты, посмотри, какой клёвый у Xiaomi. Я посмотрел, почитал обзоры. Электросамокат Xiaomi Mi Electric Scooter M365 действительно прекрасен. И я его купил.
Сяомикат
Официально этот самокат в Россию не поставляется. И вряд ли когда-нибудь будет. Но в московских и питерских интернет-магазинах серых предложений полно. За чудовищные несколько тысяч рублей (потому что тяжёлый и негабаритный) и пяток дней его охотно доставляют в Омск. Кстати, в пункте выдачи транспортной компании стояло две коробки с сяомикатом: моя и какого-то другого омича-самокатчика.
Тут есть некоторая терминологическая путаница. «Scooter» на английском — это вообще-то мотороллер, близкий родственник мотоцикла. А самокат — это «kick scooter». Как видите, в названии сяомиката слова «kick» нет. Электросамокатами у нас называют даже странные машины размером, весом, компоновкой и управлением с полноценный мотороллер, только на электрической тяге.
Сяомикат — не такой. Он — средний электросамокат. Он лишь чуть подлиннее (с метр) и повыше обычного взрослого самоката. Но тяжёлый, да, 12 кг.
С другой стороны, в продаже встречаются недоразумения с маленькими колёсами и запасом хода в 10 км. Их я не рассматриваю, как нормальное транспортное средство. Так же как и многочисленные гироскутеры (тоже скутеры, блин). Это — игрушки. Я не представляю, как на них проехать непрерывно хотя бы пару километров на хорошей скорости.
Красота
Самокат или моноколесо? Масса у них примерно одинаковая, максимальная скорость тоже, запас хода от одной зарядки тоже. Моноколесо значительно компактнее, а вот самокат в магазинную тележку уже не очень засунешь (ещё не пробовал). Но сложить, поставить вертикально, и убрать на зиму в чулан, и самокат можно. На моноколесе нужно учиться ездить, а самокат прост и интуитивен. На самокате смогут ездить не только я, но и все домашние, включая и дочу (хотя ей сяомикат ещё явно великоват). Когда сядет батарейка, моноколесо придётся тащить как чемодан, ехать оно не сможет. А электросамокат станет обычным самокатом, пусть тяжёлым и высоким, но на нём можно самокатить, а можно и просто катить рядом.
На электросамокате. Поездил до работы. Да, это то, чего я хотел. Просто катишься и ловишь кайф. Встречный ветер и подъёмы не страшны. Одной зарядки хватает на три поездки до работы и обратно. Меньше пятидесяти копеек за одну поездку в один конец в текущих ценах на электричество. По сравнению с общественным транспортом, стоимость самого сяомиката окупится лет через двенадцать :)
Самое серьёзное разочарование: заводская упаковка. Металлические крашенные детали трутся друг о друга безо всякой картонки между ними. Пока доехало до Омска, протёрлось до голого металла. Хорошо, что алюминий. Впрочем, под слоем дорожной пыли и грязи этого не видно.
У сяомиката большие (для самоката) надувные колёса. С большинством сортов плитки борются успешно, но есть места, где всё же трясёт, но с коленками всё ок. С колёсами связана самая частая известная проблема этого самоката: перетирается камера заднего колеса. Решение известно: накачивать, как рекомендовано производителем, до 45-55 psi (3.3-3.7 атмосфер), и не допускать до самоката мужиков тяжелее 100 кг. Автомобильным насосом колёса накачиваются махом. Нипель переднего колеса сильно утоплен в пластмассовый колпак, чтобы до него добраться, есть удлинитель. Только этот удлинитель без клапана, приходится производить странные манипуляции: откручивать этот удлинитель, одновременно пальцем зажимая его дырку.
На руле у сяомиката одна кнопка и четыре светодиода. Кнопка включает его. Когда он включен, одно нажатие на кнопку включает/выключает фару. Да, тут есть небольшая фара. Длительное нажатие на кнопку выключает. Двойное нажатие на кнопку включает/выключает эко-режим. Вроде как в в этом режиме экономнее расходуется аккумулятор, и максимальная скорость ограничена 18 км/ч (в обычном режиме 25 км/ч). На практике польза от эко-режима в том, чтобы его включать, давая покататься на самокате детям. Чтобы хоть бегом их догнать, в случае чего.
Светодиоды показывают заряд батареи. Начиная с верхнего, они сначала мигают, потом поочерёдно гаснут. Впрочем, нижний остаётся гореть, когда остаётся уже менее 15% батареи. Экспериментально проверено, один светодиодик «сжигается» за поездку на работу и обратно, т.е. восемь километров. Получается, что заявленный пробег от одной зарядки в 30 км весьма близок к истине.
Руль сяомиката
На правой ручке руля находится «газ». Такое поворотное кольцо с выступом, нечто среднее между поворотной ручкой и курком. В саяомикате есть неотключаемая программная фишка (Zero Start): чтобы поехать двигателем, нужно сначала оттолкнуться и разогнаться до 5 км/ч (одного хорошего толчка достаточно), лишь после этого «газ» начнёт действовать. Я нахожу это весьма полезной страховкой, чтобы самокат не вырывался из-под ездока самостоятельно, при случайном прикосновении к «газу».
На малых скоростях ускорение от электропривода весьма заметно, с непривычки хватаешься за руль. На больших скоростях самокат разгоняется значительно более вяло. Возможно, это особенность именно сяомиката, его хвалят и ругают за вялую динамику. Те, кто хвалят, считают это специальной фишкой. У меня претензий нет. С велосипедистами игру в обгонялки вы однозначно проиграете, но это и хорошо, целее будете.
Самокат очень тихий. Нет никаких трещоток, как на велосипеде. Лишь при разгоне слегка гудит как троллейбус. По плитке хорошо слышно, как резиновые колёса пересчитывают стыки.
Слева на руле — стандартный рычаг тормоза. Он задействует механический дисковый тормоз на заднем колесе. Тормоз хороший. Я тут резко затормозил перед выскочившей собакой, так меня аж занесло, на асфальте. А ещё этот рычаг активирует рекуперативное торможение передним ведущим колесом.
Тормоз сяомиката
С сяомикатом работают аж два мобильных приложения. MiHome от Xiaomi и Ninebot. Какие-то сложные взаимоотношения между этими китайскими компаниями, Ninebot купил Segway в 2015, а сам кушает деньги Xiaomi. Мой телефон не захотел увидеть самокат, а вот планшет подключился, видимо, дело в версиях Bluetooth.
В приложениях можно увидеть текущий заряд аккумулятора, текущую скорость, что может быть полезно, если прикрепить телефон к рулю, только креплений нет. Ещё показывает оставшийся пробег, в километрах. Склонен занижать этот показатель, что, безусловно, хорошо. Можно обновить прошивку, у меня оказалась самая свеженькая. Можно посмотреть серийный номер, дату производства и общий пробег самоката. Мой самокат сделали в марте 2017. На заводе его обкатывали метров девятьсот.
Можно поменять пару важных настроек. Первая: включить круиз-контроль. В обычном режиме самокат работает, как пишут, как Тесла. Нажали «газ» — он едет. Больше «газа» — едет быстрее. Отпускаете «газ» — он тормозит, рекуперативно, передним колесом. Даже красной лампочкой на заднем крыле при этом мигает. Хотите тормозить сильнее — жмите ручной тормоз левой ручкой.
А вот если включен круиз-контроль, поведение может немного поменяться. Если вы пять секунд держите постоянную скорость (что само по себе требует ровного покрытия и отсутствия резких манёвров), похоже, именно скорость, а не постоянное положение рычажка «газа», самокат издаёт писк. И после этого можете отпустить «газ». Аппарат будет поддерживать заданную скорость самостоятельно. Это очень удобно на длинных ровных пустых участках маршрута. Чтобы выйти из этого режима, достаточно чуть зацепить рычаг тормоза, тогда он начнёт тормозить двигателем, или шевельнуть «газ».
Вторая настройка: интенсивность торможения двигателем. От рождения она стояла на минимуме. Отпускаешь газ, и торможения почти не ощущается. Просто замедляется быстрее, чем накатом. Я поставил на среднее значение. Так мне показалось комфортнее. Тогда он ощутимо тормозит, но для полной остановки со скорости 20 км/ч нужно метров 20. Стало гораздо удобнее контролировать скорость, когда ползёшь за пешеходами в поисках просвета, чтобы проскочить. Можно реже прибегать к дисковому тормозу, только в экстренных случаях.
У торможения двигателем есть забавная особенность. Если, приближаясь к переходному переходу, вы спешились, не оттормозив до нуля (ну спрыгнули на ходу), рекуперативное торможение продолжает действовать. И катишь его через переход, а он явно сопротивляется, батарейку заряжает. Решение: тормозить полностью, рычагом слева.
«Газ» работает как газ в машине. Встречный ветер или подъём — «газу» надо подбавить, чтобы сохранить скорость. На круиз-контроле самокат слегка замедляется в начале подъёма, потом соображает, и нагоняет скорость сам.
Некоторые подъёмы, вроде пандусов с велосипедных дорожек, самокат не осиливает. Тупо останавливается, приходится помогать ногой. Можно поступить по-велосипедному, и взять подъём с разгона. Только надо учитывать, что клиренс — совсем не велосипедный.
В обзорах отмечают конструктивный недостаток: пластмассовая крышка снизу с торчащими винтами. Аккумулятор и контроллер у сяомиката спрятаны в деке (она достаточно толстая), и снизу прикрыты этой самой крышкой. Утверждается, что довольно легко эту самую крышку раскокать, а винты стесать. По мне, так для этого надо действительно скакать по бордюрам, чего я пока делать не собираюсь. Если что, предприимчивые китайцы уже продают металлическую крышку.
Сяомикат на просвет
Сяомикат весит 12 кг. Весьма увесистая штука. Но он замечательно складывается. Сложенный руль цепляется (внезапно, рычажком звонка, там есть звонок) за заднее крыло, и становится удобной ручкой для переноски. Так и таскаю на четвёртый этаж.
Пытались мы потягаться с велосипедом. Максимальная скорость, замеренная велокомпьютером на катящемся рядом велике, получилась 23 км/ч. Это при небольшом встречном ветре и по плитке. Имхо, для самоката — более чем достаточно, чтобы раздолбаться об столб. Велосипедист говорил, что при таком ветре такая скорость тоже несколько выше комфортной.
С другой стороны, как-то пытался обогнать опытного бегуна. Не смог. Дорожка пересекалась многочисленными мощёными участками с небольшими бордюрами. Я постоянно сбавлял скорость, чтобы аккуратно проехать по неровностям. А бегун бежал без остановок :)
Будем считать миссию выполненной. Электросамокат — отличное средство, чтобы ездить на работу в моём случае. На дорогу уходит 15-25 минут. Время зависит от количества гуляющих по набережной. Не привыкли ещё у нас люди к велодорожкам, ходят, как им удобно. Приходится аккуратно объезжать. А ещё бывает, что пространством велодорожки вынуждены одновременно пользоваться и велосипедисты, и пешеходы, и даже автомобилисты.
Посмотрим, как оно будет осенью. Самокат имеет степерь защиты IP54, что вроде как мало для нормального транспортного средства. Это значит, что в дождь на нём ездить можно, но луж надо избегать.
И зимой. Производитель заявляет возможность эксплуатации до -10 °C. В обзорах гоняют по чищенным дорожкам в зимнем парке, и хвалят, что, за счёт переднего ведущего колеса, аппарат весьма устойчив. Понятное дело, что при такой температуре ёмкость батареи упадёт раза в два. Но всё равно неплохо. Попробуем.
Сложенный сяомикат

2017-06-25

О matplotlib

Погрузился я в пучины Питона. И сделал для себя несколько открытий.
Про Flask как-нибудь в другой раз, если будет повод. А пока про другое.
Matplotlib logo
Графики. В математическом смысле. Нарисовать, показать, визуализировать. Помнится, впервые я потыкал matplotlib несколько лет назад. Когда нужно было нарисовать график по простыне чисел из логов. А вот сейчас пришлось с нею (библиотекой) познакомиться поближе.
В простейшем случае использование matplotlib выглядит очень просто.
from matplotlib import pyplot as plt

plt.figure()

x = [1, 2, 3]
y = [0.5, 1.8, 0.7]

plt.plot(x, y)

plt.show()
Этот код нарисует график по трём точкам.
figure
plt.show() попытается запустить графическое окошко, где будет показан график, и откуда можно сохранить это дело в png файл.
Можно сразу сохранить в файл.
plt.savefig('fig1.png')
И это вполне работает даже на серверах без X. Правда, для без X может понадобиться явно задать бэкенд, не использующий графику.
import matplotlib as mpl
mpl.use('Agg')
Заметьте, что всякое рисование, это вызовы функций (вроде plot()) модуля pyplot. Именно такие вызовы вы увидите во всех примерах в документации matplotlib. И во всех ответах на StackOverflow. Рисование происходит на текущем рисунке. А новый текущий рисунок создаётся вызовом plt.figure(). Вполне академичный и питоновый подход.
Но любой более-менее опытный программист отметит, что этот подход потоконебезопасен. Пока мы рисуем в одном потоке, другой поток может создать новый текущий рисунок. В академической среде, где matplotlib используется для визуализации данных, это никого не волнует. В серьёзном продакшине это тоже не создаёт особенных проблем, потому что потоками в Питоне никто не любит пользоваться, все пользуются процессами. Даже для серьёзной параллельности рекомендуют заменить threading на multiprocessing.
Но на самом деле всё рисование делается через объект Axes (не топоры, а оси), который есть у каждого рисунка (а часто и не один). А всякие манипуляции с рисунком в целом, в том числе и сохранение в файл, делается через объект Figure (не фига, а рисунок). Так что эту потенциальную потоконебезопасность можно обойти.
figure = plt.figure()
axes = figure.gca()

x = [1, 2, 3]
y = [0.5, 1.8, 0.7]

axes.plot(x, y)

figure.savefig('fig2.png')
Вернёмся к рисованию.
Если не нравится, как отмасштабировался график по умолчанию, можно задать диапазоны по осям.
axes.set_xbound(0, 4)
axes.set_ybound(0, 2)
figure
Конечно же существуют и get-методы для получения текущих значений.
Можно всяко-разно-хитро форматировать метки на осях, расставлять подписи и заголовки. Какую-то магию, вроде задания форматтеров, нужно делать до рисования. Другую магию, вроде красивого оформления меток на осях, нужно выполнять после рисования.
from datetime import date
import matplotlib.dates as mdates
import matplotlib.ticker as mtick

x = [date(2017, 6, 24), date(2017, 6, 25), date(2017, 6, 26)]   # даты
y = [50, 100, 40]                                               # проценты

figure = plt.figure()
axes = figure.gca()

axes.set_title('Tick format example')   # заголовок графика
axes.set_xlabel('Dates')                # заголовок оси X
axes.set_ylabel('Percents')             # заголовок оси Y

axes.xaxis.set_major_locator(
    mdates.DayLocator())                # местоположение меток по оси X, на каждый день
axes.xaxis.set_major_formatter(
    mdates.DateFormatter('%Y-%m-%d'))   # формат меток по оси X, в формате strftime
axes.yaxis.set_major_formatter(
    mtick.FormatStrFormatter('%.0f%%')) # формат меток по оси Y, в формате оператора %

axes.plot(x, y)

figure.autofmt_xdate()     # красиво расположить метки по оси X
figure
Можно рисовать столбчатые диаграммы. По оси X тут будут равномерно распределённые попугаи, которые обычно генерируют библиотекой numpy, которая тоже постоянно используется для матана на Python и является обязательной зависимостью matplotlib.
import numpy as np

values = [40, 60, 50]               # значения
tick_labels = ['A', 'B', 'C']       # подписи к столбцам

positions = np.arange(len(values))  # положения столбцов по оси X
width = 0.35                        # ширина столбцов

figure = plt.figure()
axes = figure.gca()

axes.bar(positions, values, width, tick_label=tick_labels,
         align='center')            # по центру — красивее
figure
Можно рисовать круговые (пироговые, pie chart) диаграммы. И добавлять легенду.
values = [40, 60, 50]       # значения
labels = ['A', 'B', 'C']    # подписи

figure = plt.figure()
axes = figure.gca()

axes.pie(values, labels=labels,
        autopct='%.0f%%')   # расставить проценты
axes.legend()

axes.axis('equal')          # чтобы круг был круглым
figure
Есть ещё довольно много типов диаграмм из коробки: графики с метками ошибок, диаграммы рассеивания, графики в логарифмическом масштабе, графики с заливкой цветом и другие.
Но вот если хочется нарисовать что-то, что не рисуется стандартными диаграммами, приходится опускаться на один уровень абстракции ниже.
Например, чтобы добавить надпись над столбцами столбчатой диаграммы, приходится вручную высчитывать координаты текста.
values = [40, 60, 50]               # значения
labels = ['one', 'two', 'three']    # надписи над столбцами
tick_labels = ['A', 'B', 'C']       # подписи под столбцами

positions = np.arange(len(values))
width = 0.35

figure = plt.figure()
axes = figure.gca()

axes.bar(positions, values, width, tick_label=tick_labels,
         align='center')

y0, y1 = axes.get_ybound()      # размер графика по оси Y
y_shift = 0.1 * (y1 - y0)       # дополнительное место под надписи

for i, rect in enumerate(axes.patches):     # по всем нарисованным прямоугольникам
    height = rect.get_height()
    label = labels[i]
    x = rect.get_x() + rect.get_width() / 2 # посередине прямоугольника
    y = y0 + height + y_shift / 2           # над прямоугольником в середине доп. места
    axes.text(x, y, label, ha='center', va='center')    # выводим текст

axes.set_ybound(y0, y1 + y_shift)   # меняем размеры графика, чтобы надписи поместились
figure
А чтобы сделать бубликовую диаграмму (или пончиковую, donut chart), придётся нарисовать круговую, а в центре нарисовать кружок. Либо вручную высчитывать координаты и рисовать сектора.
from matplotlib.patches import Circle

values = [40, 60, 50]       # значения
labels = ['A', 'B', 'C']    # подписи

figure = plt.figure()
axes = figure.gca()

axes.pie(values, labels=labels,
        autopct='%.0f%%',   # расставить проценты
        pctdistance=0.7)    # проценты чуть подальше от центра
axes.add_patch(Circle((0, 0),           # круг посередине
                      radius=0.5,       # радиусом 0.5
                      facecolor='w',    # с белой заливкой
                      edgecolor='b'))   # и чёрной окантовкой

axes.axis('equal')          # чтобы круг был круглым
figure
Таких низкоуровневых примитивов весьма много. Большинство из них — наследники класса Patch. Тут есть прямоугольники, эллипсы, многоугольники, сектора и дуги, стрелки, прямые и даже кривые Безье. Текст, оси и прочее — тоже примитивы, отдельно. В общем-то, весьма полноценная векторная графика получается.
Всё это рисуется не в пиксельных координатах, а в координатах данных. Та же круговая диаграмма — это сектора с центром в точке (0, 0) и радиусом в единицу. С одной стороны, удобно, всякие метки и прочий текст можно прицепить именно что к конкретной точке данных. С другой стороны, несколько непривычно. В конце концов, даже полярные координаты поддерживаются.
Кстати, текст можно писать в TeX.
А ещё matplotlib очень круто работает с цветами. Помимо возможности задавать цвета в виде RGB, RGBA (да, есть прозрачность), HSV, в нотации HTML, в виде кучи предопределённых имен, есть ещё и карты цветов (colormap). Когда вы рисуете кучу графиков вместе, вам же хочется каждую линию покрасить в свой цвет, и чтобы всё вместе смотрелось красиво. Вот matplotlib и имеет кучу предопределённых палитр на любой вкус. Конечно же, можно создать и свою.
Можно делать рисунки, содержащие много графиков. При этом в рамках одной Figure создаётся именно множество Axes.
import matplotlib.gridspec as gridspec

figure = plt.figure()
grid = gridspec.GridSpec(3, 3)   # сетка 3х3

figure.suptitle('Many graphs')   # общий заголовок

axes0 = figure.add_subplot(grid[0, 0])      # ячейка в 0 строке и 0 столбце
axes0.set_title('cell')

axes1 = figure.add_subplot(grid[0, 1:])     # ячейка в 0 строке с 1 столбца и до конца
axes1.set_title('colspan')

axes2 = figure.add_subplot(grid[1:, 0])     # ячейка c 1 строки и до конца в 0 столбце
axes2.set_title('rowspan')

axes3 = figure.add_subplot(grid[1:, 1:])    # большая ячейка
axes3.set_title('big cell')

figure.tight_layout()   # чтобы не налезали друг на друга
figure
matplotlib либо показывает графики в примитивном GUI, либо генерирует картинки. Но у нас ведь веб (2.0). А в вебе есть потрясная библиотека d3(.js). И неудивительно, что захотелось скрестить ужа с ежом. Создавать диаграммы в matplotlib, а рисовать их в браузере через d3. Этим занимается библиотечка mpld3.
Принцип работы интересный. Из рисунка (Figure) из matplotlib создаётся некий JSON, питоновой библиотекой. Этот JSON скармливается JS библиотеке, которая использует (и зависит от) d3, чтобы нарисовать что нужно. Питоновая часть mpld3 может сама сгенерировать весь HTML, достаточный для запуска в браузере.
from matplotlib import pyplot as plt
import mpld3

figure = plt.figure()
axes = figure.gca()

x = [1, 2, 3]
y = [0.5, 1.8, 0.7]

axes.plot(x, y)

mpld3.show(figure)
show() запускает сервер и открывает браузер. Но у меня это не заработало, потому что этот сервер отдаёт минифицированную версию d3.js c не-ASCII символами, не указывая кодировки.
Надо, конечно же, не использовать этот примитивный сервер. Можно, к примеру, взять тот самый JSON, который генерирует mpld3, и самостоятельно передать его в веб страницу.
mpld3_json = mpld3.fig_to_dict(figure)
figure.clear()      # освободить ресурсы matplotlib
return jsonify(mpld3_json)
А на странице, где уже подключены d3.js и mpld3.js, выполнить небольшой яваскрипт.
!function(mpld3){
    mpld3.draw_figure('{{ div_id }}', {{ mpld3_json | tojson }});
}(mpld3);
Этот скрипт нарисует график в div с указанным id. Соответственно, можно на одной странице показывать несколько графиков.
Всё прекрасно и хорошо. mpld3шные графики даже как-то интерактивны, зумятся, можно добавить всплывающие подсказки. Но mpld3 не поддерживает заметную кучу возможностей matplotlib.
В mpld3 нельзя задать форматирование подписей и цвет отображения осей. Это очень портит дело, когда у вас не числа, а, например, даты, которые очень нужно форматировать. Да что там, в mpld3 нельзя скрыть оси, что актуально, например, для круговых диаграм. Нафиг там не нужны чёрные оси слева и внизу. Максимум, что можно, хоть убрать числа с осей.
axes.set_axis_off()  # not supported by mpld3
axes.get_xaxis().set_ticks([])
axes.get_yaxis().set_ticks([])
В mpld3 нельзя задавать общий заголовок для рисунков с множеством графиков. Плюс ещё куча мелких проблем со шрифтами, штриховыми линиями и прочим. 3D графики mpld3 тоже не умеет.
Вообще хочется, раз уж мы пошли графики в браузере делать, выкинуть все эти питоновые прослойки, и рисовать напрямую в d3. Правда, придётся кодить на JavaScript, а не на Python. А ещё говорят, что где-то есть мифические аналитики, которые привыкли matplotlib. Так что...
Как же налаживать и отлаживать все эти графики? В Jupyter Notebook конечно же. Также почему-то когда-то известном как IPython.
Ставим и запускаем где-нибудь в папочке, где будем ноутбуки складировать. Актуальная версия только для Python 3, но можно доставить ядрышки, и запускать код во втором Питоне.
$ sudo pip3 install jupyter
$ cd ~/notebooks
$ jupyter-notebook
А дальше начинается интерактивная магия. Можно писать куски кода на Питоне, тут же выполнять их, тут же видеть результат. Энтузиасты машинкового обучения только в этих блокнотах и живут.
Можно и подробнейшие комментарии в Markdown вставлять, в отдельные ячейки. С поддержкой формул в TeX, конечно же.
Чтобы рисунки matplotlib сразу отображались в блокноте, нужно добавить директиву. Тогда никаких plt.show() не понадобится. Любое наличие Figure в ячейке приведёт к выводу картинки.
from matplotlib import pyplot as plt
%matplotlib inline
Чтобы вместо изображений matplotlib отображались SVGшки mpd3, можно явно вызвать метод display().
import mpld3

# ...

mpld3.display()
Либо же перманентно включить рендеринг любого matplotlib через mpld3.
import mpld3
mpld3.enable_notebook()
Понятно, что и matplotlib, и mpld3 должны быть доступны для движка блокнота.
Физически блокнот представляет собой файл с расширением .ipynb. В формате JSON, на самом деле. Все изменения периодически сами туда сохраняются. В файле хранится не только код, но и все результаты, даже картинки matplotlib. Так что для просмотра блокнота вовсе не обязательно в нём что-то запускать. И эти файлы вполне можно коммитить, GitHub их даже показывать умеет.
Вот, к примеру, ноутбук с примерами из этого поста.