iPad Proでラフな絵を書きたい

絵と言ってもイラストっぽいやつではなく、技術書などに出てくるような、主に線と四角形と若干のテキストで構成されてるようなやつを、さくっと手書きで書きたい。こんなのだ。

f:id:atsuoishimoto:20200109094029p:plain:w300

ちょっと長めの英単語や文章が入ることもあるので、そういった部分はキーボードで入力してテキストボックスで配置したい。

現在、こういった絵を書くときには、Notability を使っている。機能的にはこれで十分だし、もっと本格的なお絵かきツールの Procreate なども持っているが、自分にはNotabilityがあっているようだ。

とはいえ、Notabilityも本質的にはメモ取りアプリ。お絵かき機能はさほど高くない。Procreateでは高機能すぎて使い方がわからないし、起動も重たい。Notabilityよりは高機能で、それほど重たくない、そんな好都合なアプリはないだろうか?ということで、ちょっと探してみた。

GoodNotes

よくNotabilityと並んで紹介されているメモアプリで、お絵かき機能はNotabilityよりやや高機能。

しかし、Notabilityは絵を描くと自動的にキャンパスが拡張されるのに対して、GoodNotesはページを追加する、というオペレーションが必要で、これが邪魔。わざわざ紙のノートを模倣して、不便にしなくてもいいと思うんだけどな。

Paper

  • お金を払えばいろいろ高機能なようだが、無料だと大したことはできない。色もペンもプリセットだけ。

  • テキストボックス機能がない。

  • 指でこするとぼかしになるのは面白いが、別にそういう機能は求めてない。

Microsoft OneNote

かの高名なMicrosoft Office一族のOneNote様だが、果たして…。

  • Officeアカウントにログインしないと使えないが、iOSのパスワード自動入力機能が使えない。だめすぎ。

  • ページを選択するUIが、やたらと横幅をとって使いにくい。あとページのサムネイルぐらい表示しろ。

  • iPadにリボンツールバー持ってくるのやめろ。ページ切り替えてボタンクリックとか使いにくいわ。

  • これは気のせいなのだろうか?選択した図形をコピペできない。描画した図形を選択はできるが、「コピー」のメニューが出てこない。まさかとは思うが、コピーできないのか?そんなお絵かき機能ってあり得るのか?

Skitch

Evernote用のお絵かきアプリ。2015年以来更新されていない。Penultimateに移行した模様。

Penultimate

Evernote用お絵かきアプリ

  • ペンが接続されていても指で描画できてしまう。ペンあるんだから指はスクロールとか消しゴムかなんかにしてほしい。

  • テキストボックス機能なし。

  • 図形を選択してリサイズできない。

concepts

  • ペン接続してても指でかけてしまう。

  • テキストボックス機能なし。

  • 有料版への誘導がうざい。

メモ

iPadに標準で入ってるやつ

  • テキストボックスなし。

  • 選択画像のリサイズができない。

総評

なんだかんだいって、Notabilityが一番不満なく使えるようだ。しかし、もうちょっと、例えばブラシの種類を選べたり、テキストボックスが使いやすかったりすると嬉しいんだけど…

Software Designの原稿をJupyter NotebookとiPad Proで書いた話

雑誌 Software Designさんから執筆依頼を頂いた。「Visual Studio Code の Jupyter Notebook実行機能を使ってPythonのテキスト処理などを学べる記事を」ということで、得意なテーマだしスケジュールに余裕のある時期でもあったので、けっこう気軽に引き受けさせていただいた。

この記事は、プログラミング初心者が雑誌を見ながらコードを一文字づつJupyter Notebookに写経して実行する、という読み方を念頭に執筆した。なので、文章を読みながら出てきたコードを入力し、Shift+Enter で実行できるように留意している。そこで、記事文章もJupyter NotebookのMarkdownセルで執筆し、解説とコードと実行結果をすべて Jupter Notebookだけで管理してみた。

せっかくVSCodeとJupyter NotebookをPythonのフロントエンドとして、という企画なので、執筆もVSCode上のJupyter Notebookでやろうと思ったが、現在のバージョンではMarkdownセルを編集中に別のセルに移動すると入力した文字が消えてしまう、という致命的な問題があり、断念した。

執筆手順

解説の文章とサンプルコードの執筆については、特に語ることはない。普通にMarkdownセルに文章を書き、Pythonセルにコードを書いて、実行結果を出力するだけだ。

普通にreStructuredTextやMarkdownなどのマークアップだけで書いていると、サンプルコードや実行結果を原稿にコピーするのは結構な手間だが、Jupyter Notebookでは当然その作業は不要だ。記事の執筆そのものはサクサク進み、かなりのスピードで書き上げることができた。

図の作成

今回は、記事にかなり多めに図を入れさせていただいた。初心者向けの記事であり、文章ではわかりにくいことも、図示すればけっこうわかりやすかったりする。今回は正規表現の解説も行ったが、経験上、正規表現のパターンマッチングをうまく理解できない人でも、ちょっと絵を書いてあげるとすんなり納得してくれることが多い。

とはいえ、私が図をきれいに書いていると、とても時間がかかってしまう。どうせ編集部の方で清書していただけるのだから、手書きでサクッとかきあげ、入稿させた頂いた。図は iPad Pro+Apple Pencilに、Notabilityというアプリで描いた。Notabilityは作画アプリではなく手書きメモアプリだが、凝った絵を描くことはないので十分だった。こんな図だ。

f:id:atsuoishimoto:20191206153218p:plain

こんなのでも、普通にパソコンでマウスを使って描いていたら、一枚あたりそこそこ時間がかかってしまう。しかし、手書きならあっという間だ。ざっと描いて選択し、クリップボードにコピーする。すると、この画像はメイン環境のMacbookで実行しているJuputer Notebookのマークダウンセルにペーストできてしまう。あっというまに図入りのMarkdownが完成だ。画像の作成・取込み作業としては、ほぼ最小手数なのではないだろうか。

f:id:atsuoishimoto:20191206154348p:plain:w400

このぐらいの手間で済むなら、図なんかなんぼでも描けてしまう。絵の汚さに我慢できればの話だが。調子に乗って、汚い字で描きまくってしまった。編集部の皆様の負担になったのではないかと思うと、大変申し訳無い次第だ…

問題点

しかし好事魔多し。そう都合の良いことばかりは起きないもので、ちょっとした問題点にもぶつかった。

一つは、Markdownのセル一つには、画像を一つだけしかペーストできない、という点だ。操作上はいくつでも画像を張り込めるのだが、内部的には最後にペーストした一つだけしか保存されていない。まあ、セルを分ければ良いのだが、あまりこういう使い方はしたことがなかったので知らなかった。

もう一点はJupyter NotebookというよりはJupyter NotebookをSphinxに取り込んで文書化するnbsphinxの問題だと思うが、Notebookに別々のセルであっても同じ名前の画像ファイルがあると、うまく画像を表示できない。Jupyter Notebookに画像をペーストすると、すべてimage.png という名前で保存されてしまうので、結局画像は一つしか使えない、ということになってしまう。

入稿もJupyter Notebookで可能、という点は確認済みだったのでPDF化は必須ではないが、原稿のチェックをするときにはPDFは便利だ。できればSphinxを使いたい。

このため、別途簡単なスクリプトを作成し、ipynbファイルを読み込んで、画像ファイルの名前とMarkdownマークアップを修正してからビルドするようにする必要があった。

書き上げてみて

Jupyter Notebook + iPadでの原稿執筆はなかなか快適で、気がついたら予定のページ数を大きく超過してしまい、後の削除が大変だった。一年ぐらい寝かしたら、削除部分も復活して完全版をどっかに公開するかもしれない。

しかし、Jupyter Notebookは、最初の執筆はあまり問題ないが、仕上げの推敲がめんどうだ。しょせんブラウザ上のエディタなので高度な編集機能は使えないし、テキストがセル単位に細かく分割しているため、広い範囲の修正には不便だ。また、文章の順番の入れ替えなどもエディタほど簡単ではない。

今回は30ページほどの記事なので大したことはなかったが、これが100ページ200ページとなると、なにか対策が必要となるかもしれない。

その他の問題

Jupyter Notebookとは今回の記事ではテキストのエンコーディングも解説していて、その中でいわゆるシステム固有文字の㈱や①をどうしても使いたかった。

しかし、デフォルトのSphinxではpLaTeXという、システム固有文字をサポートしていないツールが使われている。LaTeXで丸付き文字を出力することは可能なので、この機能をSphinxから利用できないか相談してみたところ、なんとSphinxメンテナの小宮氏から、upLaTeXというユニコードをサポートしているLaTexで出力する方法を教わり、事なきを得た。Sphinxconf.py に次の設定を加えることで、upLaTeXが使われるようになる。

language = 'ja'
latex_elements = {
    'classoptions': ',uplatex,dvipdfmx',
}

小宮氏には厚くお礼申し上げる次第である。

f:id:atsuoishimoto:20191206160233p:plain:w350

Python 3.8 の概要 (その8) - Did you mean "=="?

さて、質問です。

a = 1.0
a is 1.0

上記の処理で、a is 1.0 の結果は True となるでしょうか、それとも False となるでしょうか?

True と答えたあなた、不正解です。反省してください。

False と答えたあなた、同じく不正解です。猛省してください。

正解は 「わからない」 です。

Pythonインタープリタを起動して、対話的に実行してみましょう。

>>> a = 1.0
>>> a is 1.0
False

False ですね。a に代入した float オブジェクトと、a is 1.0 で比較している float オブジェクトは、同じ値ですが異なるオブジェクトです。

でも、ちょっと書き換えて、同じ処理を関数の中で実行するとどうでしょう?

>>> def test():
...     a = 1.0
...     return a is 1.0
...
>>> test()
True

True ですね。まったく同じように書いているのに、グローバルに実行する場合と、関数内で実行する場合では、結果が異なっています

このように、定数を is で比較しても、意味のある結果は得られません。 プログラミング初心者が、変数が同じ値かどうか調べるのに is 演算子を使ってしまうことがありますが、これは間違いです。==を使わなくてはなりません。

>>> a = 1.0
>>> a == 1.0
True

だいぶ前に is演算子の不思議で解説しましたが、スクリプト中で 1.0"hello"などの数値や文字列、タプルなどの定数を書いたとき、対応する数値オブジェクトや文字列オブジェクトを新しく作るのか、それとも作成済みのオブジェクトを使い回すのか、言語仕様では決められていません。つまり、1.0 is 1.0 という式の値はやってみなければわからないし、やってみても次にやったときに同じ結果になるとは限らないのです。

ということで、Python3.8 では、このような比較を行うと警告が表示されるようになりました。

>>> a = 1.0
>>> a is 1.0
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False

「はい? 正気ですか? is ? == の間違いじゃないですか? 定数ですよこれ?」と言われてるわけです。素直に == を使いましょう。

is が使える場合

と書きましたが、is での比較が推奨されている場合もあります。

https://www.python.org/dev/peps/pep-0008/#programming-recommendations では、

Comparisons to singletons like None should always be done with is or is not, never the equality operators.

(None などのシングルトンとの比較は、 == ではなく、常に isis not で行います)

とされています。

上で書いたように、1.0abc などの定数値オブジェクトは、どんなときに新しく作られ、どんなときに作成済みのオブジェクトを使い回すのか、決まっていません。

しかし、例外として、「オブジェクトが常に一つだけ存在する」と言語仕様で決められている値があり、「シングルトン」 と呼ばれます。シングルトンなオブジェクトは、Python開始時に一つだけ作成され、終了するまで削除されずにそのオブジェクトを使いまわします。

このようなシングルトンな値との比較は、同じ値のオブジェクトは一つだけしか存在しないので、== ではなく is を使っても安全です。

シングルトンの代表例として、None があります。NoneNoneType 型のオブジェクトですが、NoneType 型はインスタンスを一つだけしか持たず、None is Noneという式は、常に Trueとなることが保証されています。

ですから、シングルトンな値との比較の場合は、is 演算子で比較しても警告は出力されません。

>>> a = None
>>> a is None
True

シングルトンには、他にも TrueFalse があります。TrueFalsebool 型のオブジェクトですが、どちらも常にひとつだけ存在する事になっています。したがって、この場合も True is TrueFalse is False の結果は常に Trueとなることが保証されていますので、 is で比較しても安心です。

>>> a = True
>>> a is True
True
>>> a is bool('1000')
True

bool 型のオブジェクトは TrueFalse の2つのインスタンスが存在しますので、デザインパターンでいう「シングルトン」とはちょっと違う気がしますが、それはともかくPythonでは TrueFalseis で比較する慣習になっています。

シングルトンにはもう一つ、Ellipsis が存在します。 普通のアプリケーションだとあんまり比較することはなさそうですが、これも is で比較できます。

>>> a = ...
>>> a is ...
True
>>> ... is Ellipsis
True
>>> ...is...
True

ところで、

>>> ...is...is...is...is...is...is...is...is...is...
True

これ、意味は全くありませんが一見Pythonに見えなくて好きです。

Python 3.8 の概要 (その7) - ちょっと便利そうな機能追加

math.prod()

リストなどのイテレータの要素の積を計算する math.prod() が追加されました。 sum() の掛け算版ですね。

>>> import math
>>> math.prod([1,2,3,4])
24

正規表現\N{名前} 記法をサポート

reモジュールで、正規表現に文字の名前を指定する \N{名前} を使えるようになりました。

>>> re.match(r'\N{LATIN SMALL LETTER A}', 'a')
<re.Match object; span=(0, 1), match='a'>
>>> re.match(r'\N{GRINNING FACE WITH SMILING EYES}', '😁')
<re.Match object; span=(0, 1), match='😁'>

文字の名前は、unicodedata.name() で調べられます。

>>> import unicodedata
>>> unicodedata.name('a')
'LATIN SMALL LETTER A'
>>> unicodedata.name('😁')
'GRINNING FACE WITH SMILING EYES'

importlib.metadataモジュール

importlib.metadata モジュールが追加され、pip でインストールされたパッケージの情報を取得できるようになりました。

>>> from importlib.metadata import *
>>> for dist in distributions():
...     print(f"{dist.metadata['Name']}: {dist.version=}")
...
chardet: dist.version='3.0.4'
pip: dist.version='19.2.3'
certifi: dist.version='2019.6.16'
idna: dist.version='2.8'
urllib3: dist.version='1.25.3'
requests: dist.version='2.22.0'
setuptools: dist.version='41.2.0'

コンテキストマネージャでProfile

cProfile.Profile() を、コンテキストマネージャとして使えるようになりました。

>>> import cProfile, pstats
>>> import urllib.request
>>> with cProfile.Profile() as pr:
...     with urllib.request.urlopen('http://www.python.org') as f:
...         print(len(f.read()))
...
48914
>>> pr.print_stats(pstats.SortKey.CUMULATIVE)
         4433 function calls (4380 primitive calls) in 0.183 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.149    0.149 request.py:139(urlopen)
      2/1    0.000    0.000    0.136    0.136 request.py:506(open)
      5/3    0.000    0.000    0.134    0.045 request.py:495(_call_chain)
        2    0.000    0.000    0.134    0.067 request.py:535(_open)
        2    0.000    0.000    0.133    0.067 request.py:1275(do_open)
        2    0.000    0.000    0.088    0.044 client.py:1218(request)
        2    0.000    0.000    0.088    0.044 client.py:1223(_send_request)
        2    0.000    0.000    0.087    0.044 client.py:1205(endheaders)
        2    0.000    0.000    0.087    0.044 client.py:995(_send_output)
        2    0.000    0.000    0.087    0.044 client.py:936(send)

コンテキストマネージャとしているのは、cProfile.Profile() のみで、profile.Profile() は使えません。気が付かないうちに、cProfileとprofileの差が大きくなっているようです。

Python 3.8 の概要 (その6) - 拡張モジュール関連

拡張モジュールがリリースビルド/デバッグビルドで共用可能に

これまで、デバッグ用にビルドされたPythonでは、Pythonのメモリ使用状況を調査するための機能 が有効になっていました。このため、リリース用にビルドされたPythonデバッグ用にリリースされたPythonでは、内部のデータ構造が一部異なっており、拡張モジュールのバイナリもリリースビルド用とデバッグビルド用を別々に作成する必要がありました。

Python3.8のデバッグビルドではこの機能がオフになり、リリースビルド用の拡張モジュールをデバッグビルドでも利用できるようになりました。これまで、デバッグビルドのPythonで調査するときには使用する拡張モジュールもすべてデバッグビルド用に再構築していましたが、この作業が不要になりました。

拡張モジュールが共有ライブラリ版とスタティック版で共用可能に

Pythonの構築方法には、Python本体を共有ライブラリとして実行ファイルとは別のファイルに作成する 共有ライブラリ版 と、すべてを実行ファイルで持つ スタティック版 があります。

これまで、共有ライブラリ版のPythonで拡張モジュールをビルドすると、拡張モジュールもPython本体の拡張モジュール(libpython3.x.soなど)にリンクされていました。しかし、スタティック版のPythonでは libpython3.x.so が存在しないため、共有ライブラリ版用にビルドした拡張モジュールは、スタティック版では利用できませんでした。

しかし、拡張モジュールと libpython3.x.so をリンクする必要があるのはかなり特殊なケースに限られるため、利便性を優先して拡張モジュールは libpython3.x とリンクしないように修正されました。これにより、共有ライブラリ版でもスタティック版でもおなじ拡張モジュールを利用できるようになります。

拡張モジュールのバージョン間の互換性を放棄

これはあまり今どきのユーザには関係のない変更だと思いますが、感慨深かったので取り上げます。

従来、Pythonの拡張モジュールは、古いバージョンのPython用にビルドされた拡張モジュールでも、新しいPythonで利用できるようになっていました。例えば、Python 2.0である拡張モジュールをインストールし、そのあと Python 2.0を削除して Python 2.1をインストールしても、そもまま問題なく利用できるようになっていました。

いまではこんな使い方をする人はいないと思いますが、インターネット接続が今ほど普及しておらず、また PyPIpip もない時代には拡張モジュールのビルド・インストールはけっこうな難作業で、できるだけ簡単に使い回せるようになっていました。

しかし、いまでは共有ライブラリを使い回すよりも、うまく共有ライブラリをプロジェクト間で分離するほうが重要な時代となりました。主要なパッケージはバイナリが提供されるようになっていますので、ビルド・インストールの手間も最小限です。

そこで、Python3.8から、旧バージョン用の拡張モジュールは、新しいバージョンでは利用できなくなりました。もともと、実際に複数バージョンで利用できる拡張モジュールはそれほど多くはなかったと思われますが、公式に動作を保証しない、と表明されました。

2009年頃までは、共有モジュールの互換性を確保するためにこんな PEP が出ていたぐらいですが、その後の環境の変化で、ついにこのPEPも不要と成り果てました。今となってはユーザに関係ある話ではないのですが、Pythonの歴史を紹介する意味でちょっと解説してみました。

Python 3.8 の概要 (その5) - デバッグ用 f文字列フォーマット

Python3.8の新機能で、これ一番好きかも。このためだけにPython3.8必須にしてもいい。

通常、 f文字列 に変数名や式を指定すると、その値が文字列に埋め込まれます。

>>> foo, bar = 10, 20
>>> print(f'value is {foo+bar}')
value is 30

便利な機能ですが、デバッグ用にデータを出力するときには、ちょっと面倒です。たとえば foobar の値を確認するときは、確認したい変数名のテキストと、表示したい式を別々に書く必要があります。

>>> print(f'foo={foo} bar={bar} foo+bar={foo+bar}')
foo=10 bar=20 foo+bar=30

そこで、f文字列に出力指定方法が追加され、出力したい式に続けて = を指定すると、その式と式の値の両方が文字列に埋め込まれるようになりました。

>>> print(f'{foo=} {bar=} {foo+bar=}')
foo=10 bar=20 foo+bar=30

出力フォーマットを指定する場合は、= に続けて記述します。

>>> print(f'{foo=:0.3f} {bar=:.1e} {foo+bar=:04d}')
foo=10.000 bar=2.0e+01 foo+bar=0030

Python 3.8 の概要 (その4) - multiprocessing.shared_memory モジュール

multiprocessing.shared_memory モジュールで、共有メモリを使ってプロセス間でデータを交換できるようになりました。似たような処理は mmap モジュールで実現できましたが、マルチプラットフォームで簡単に利用できるようになります。

Numpyの ndarray オブジェクトを複数のプロセスで共有する場合、まず最初のプロセスで次のように共有メモリを作成します。この例では、共有メモリの名前は "sharedmemory_test1" とします。

import math
from multiprocessing import  shared_memory
import numpy as np

SHAPE = (3,3)

# 共有メモリ "sharedmemory_test1" を作成
size = math.prod(SHAPE) * numpy.dtype("float").itemsize
shm = shared_memory.SharedMemory(create=True, size=size, name="sharedmemory_test1")

# 共有メモリを利用するndarray オブジェクトを作成する
arr = np.ndarray(shape=SHAPE, dtype=float, buffer=shm.buf)

# ndarray を初期化
arr[:] = 0

他のプロセスでは、"sharedmemory_test1 という名前を指定して共有メモリを参照できます。

from multiprocessing import  shared_memory
import numpy as np

SHAPE = (3,3)

# "sharedmemory_test1" を指定して共有メモリを作成
shm = shared_memory.SharedMemory(name="sharedmemory_test1")

#nbarrayオブジェクトを作成
arr = np.ndarray(shape=SHAPE, dtype=float, buffer=shm.buf)

# 共有メモリを参照
print(arr[0:0])

# 共有メモリを開放
shm.close()

共有メモリが不要になったプロセスでは、close() メソッドでリソースを開放します。

SharedMemoryManager

共有メモリが不要になったら、 unlink() を一度だけ呼び出して、共有メモリを削除します。

SharedMemoryManagerを使うと、共有メモリを管理する専用のプロセスを起動し、共有メモリの寿命を制御できます。

from multiprocessing import Process
from multiprocessing.managers import SharedMemoryManager

def func1(shm, arg):
    shm.buf[0] = arg
    shm.close()

if __name__ == '__main__':

    # 共有メモリ管理プロセスを起動
    with SharedMemoryManager() as smm:

        # 共有メモリを作成し、初期値を設定
        shm1 = smm.SharedMemory(10)  
        shm1.buf[0] = 0

        # func1を異なるプロセスで起動
        p1 = Process(target=func1, args=(shm1, 100))

        # func1を異なるプロセスで起動
        p2 = Process(target=func1, args=(shm1, 200))

        p1.start()
        p2.start()

        p1.join()
        p2.join()
        
        print(shm1.buf[0])