kngenieの日記

日記だ。でもさすがに毎日は書けない。

PEX作成中に死ぬ

pexの実行中/tmpに展開したディストリビューションパッケージを読もうとしてPermission deniedで死ぬ。前は普通に動いていたのだが...

```
Traceback (most recent call last):
File "/usr/lib/python2.7/atexit.py", line 24, in _run_exitfuncs
func(*targs, **kargs)
File "/home/kenji/.pyenv-xenial/versions/cdx_writer/local/lib/python2.7/site-p
ackages/pex/common.py", line 75, in teardown
self._rmtree(td)
File "/usr/lib/python2.7/shutil.py", line 247, in rmtree
rmtree(fullname, ignore_errors, onerror)
File "/usr/lib/python2.7/shutil.py", line 239, in rmtree
onerror(os.listdir, path, sys.exc_info())
File "/usr/lib/python2.7/shutil.py", line 237, in rmtree
names = os.listdir(path)
OSError: [Errno 13] Permission denied: '/tmp/tmp5m9J2E/warctools-cdx-writer'
```

調べた結果これは2018-07-23に 
Issue 15795: Zipfile.extractall does not preserve file permissions - Python tracker への対処として追加されたコードが原因。
zipからpermission bitsを読み取って展開したファイルに反映しようとしているのだが、`external_attr`に正しい値が入っていることを前提にしている。

あろうことか、githubから`archive/.zip`でダウンロードしたzipは`external_attr`のpermission bitsが基本ゼロになっており、それをそのまま使ってchmodするものだから、ディレクトリにアクセスできなくなる。
どっちが悪いとも言いにくいが、pex側で対処するのが望ましい、のだろう。

とりあえずgithubからの直接ダウンロードをやめる方向で対処する。

Chromeをハングさせる詐欺サイト

ネットをうろついていてうっかり「あなたのPC、ウィルスに観戦してますよ! サポートに電話してください」という詐欺広告を踏んでしまった。ご丁寧にブラウザをハングさせてヤバい雰囲気を出す手まで使っている。これを踏むとブラウザを強制終了するしかなくなってしまうので、なかなか尻尾がつかめないのだが、DevToolを開いた状態で自分から詐欺サイトを踏みに行き、速攻でポーズすることで中を見ることができた。

f:id:kngenie:20190301164821p:plain
詐欺サイトをDevToolsでポーズ

こんなコードが書いてあった:

var total = "";
for( var i = 0; i < 100000; i++) {
    total = total + i.toString();
    history.pushState(0,0 total );
}

お情けか上限があるのだが、200回も回ればChromeは反応がなくなって、PCのファンがブンブン回り出す。たったこれだけでこうも操作不能に陥るとは。

一応この問題は認識されているらしく、pushStateの行に対してコンソールに "Throttling navigation to prevent the browser from hanging. See https://crbug.com/882238."とメッセージが出ている (リンクされているページ見に行ってもPermission deniedとはどういうことだ)。しかし、throttlingの効果はないに等しい。

ちなみにこの詐欺サイトは複数のIPアドレスのURLで動作しており、時間が経つと消えてしまう。防御がやりにくい。逆引きしたらaf-ys-ben-us-uk-money-4.35などという名前が返ってきた。まったくサイテーである。

頭の体操

古いネタで恐縮だが、
www.softantenna.com
という話があって。元ネタはRedditだそうなんだけど(Reddit見ない)。

面白いのは問題5で、ほとんどの人はこれだけをやって楽しんでいるのではないか。日本人はもともとこの種の数字遊びが好きで、切符の端に書かれた4つの整数を見るとあれこれいじって10にする、という頭の体操をやってしまう。以前Twitterでもこのネタが流れてきて、数字の間に文字はなんでも入れていいという話だったので、「√」だの「**」だの「%」だのを入れて幾つも作って投げた覚えがある。

で、問題5だが、Python 2で書くとこんな感じか...

from itertools import product

ops = [['', '+', '-']] * 8

for p in product(*ops):
  exp = ''.join('{}{}'.format(i+1, p[i]) for i in range(8))+'9'
  if eval(exp) == 100:
    print exp

実行してみたら(2年ぐらい前のMBP) 0.14秒程度であった。3**8=6561組み合わせぐらいだからこんなもんか。

evalを使っているのがちょっと卑怯っぽい。operatorでも使って書き直して見るか。

問題3は有名なエラトステネスの篩を書きなさい、という話なんだけど、以前これをエンジニアの採用テストに出したことがあって、けっこう痛い回答を書いてくる人がいることがわかった。

私の想定する回答はこんな感じであった:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('n', type=int, help='upper bound')

args = parser.parse_args()

sieve = list(range(1, args.n + 1))

for i in (n for n in sieve if n > 1):
    for j in range(i + i, args.n + 1, i):
        sieve[j - 1] = 0

for n in sieve:
    if n > 0: print(n)

コメントがないと今見てもさっぱりわからない :D。
argparseなんか使っちゃってるが、ここらへんをスっと書いてくるかどうかも見ていたりする。

なぜPython3にはbytes.formatがないのか

1年ぐらい前に書いてドラフト箱にほったらかしになっていた。もったいないので公開する。

ずっとPython 2.7をメインにしていて特に問題もなかったのだが、最近周りがPython 3.4をターゲットにコードを書き始めていて、私の提供するライブラリが2.7しかサポートしていないと知ると悲しい顔をされるようになったので、Python 3互換にする作業をしている。かーなり面倒な作業である。いかにPython 2で文字列の扱いがいい加減だったかを思い知らされる。

Python 3互換にする上で一番苦労するのが、

  • bytes.format()がない
  • WSGI周りの仕様変更

ではないか。他はsixなどが便利に対応してくれるのだが、この二つは多くの変更を強いられる。とくに後者はfrom __future__ import unicode_literals入れときゃ後で楽、と思っていたのがバックファイアしていて痛い。

bytes.formatがないのはPython 2であたりまえだったstr/unicodeの考えなしの混用を避けるためのものだと思っていたが、Webアーカイブを扱っていると、intを文字で出力する場合など、「なんでstrで作ってからencodeしなけりゃいけないんだよ」と思うこともある。これは多分にstrとunicodeがほぼ交換可能なものとして存在したPython 2のせいで、Javaのように始めからStringとbyte[]がまったく別物であるなら悩みもしないのだが。

というところで、同僚が Issue 3982: support .format for bytes - Python tracker のことを教えてくれた。2008年9月27日に始まった「Python 3にbytes.format()を追加してくれ」というissue itemなのだが、最新のコメントは2016年6月10日である。

議論を総括すると、bytes.format()はネットワークプロトコルやファイルフォーマットを扱う上でとても便利である(というか便利すぎる)ということだ。テキスト処理のオモチャプログラムを書いているのならともかく、実世界ではほとんどのソフトウェアはネットワークプロトコルやファイルフォーマットを扱うので、Python 2にbytes.format()があったことはコードの可読性に大きく貢献した。一応Python 3にもstruct.packがあるのだが、bytes.format()に比べると機能不足が否めない。

http://bugs.python.org/issue3982#msg171804 にSerhiy Storchakaが説明しているように、str.format()bytes.format()は要件が大きく違っていて、同じメソッド名であるべきかどうかも疑問である。struct.packを拡張した方がいいのではないか、という提案もうなづける。ただ、テキストベースのプロトコルやファイルフォーマットが当たり前な昨今、struct.packという名前はあまり感じがよくないし、http://bugs.python.org/msg180423 でGuidoが書いているように、問題は主にPython 2/3両互換のコードを書くことなのだから、メソッド名が違うと解決にならない。

Python 3.3ではasciilatin1へのencodeはmemcpyと同程度に高速である。以前のバージョンに比べて12倍以上速いという。これはPEP-393に見るように、Pythonの文字列表現がASCIIに収まる文字列に対して最適化が行われているためだろう。これを根拠に「str.format()してからencode()しろ」という意見が当初は大勢を占めた。

Python 3のstr.format()はunicodeを前提に作り込まれていて、__format__による拡張など、bytes.format()の実装を面倒にするポイントが多々あるようだ。ただ、これはGuidoが指摘するように、bytes.format()はベーシックなデータタイプしかサポートせず、__format__も使わない、とすればいいだけのように思える。

bytes.format反対が優勢の議論はGuidoがTwistedのニーズを紹介したあと、Glyph Lefkowitzが議論に参加して変化を見せる。http://bugs.python.org/issue3982#msg180447 に至って、Terryがbytes用の限定的formattingの必要性を理解を示した。それまでstr.formatと同じものを実装することを求められていると思っていたらしいのが、Python 2/3両互換のために限定的な機能を用意すればいいだけだ、という共通理解が形成されていく。さらにformatより__mod__(いわゆる%-formatting)にするべきという意見も出てくる。確かにformatPython 2に導入されたのはつい最近なので、__mode__を使っているケースの方が多い。

結局Antoine Pitrouによるbytes.format()の試験的な実装提案(PEP-460 Add binary interpolation and formatting, 2014年1月6日)は
同時期に起案されたPEP-461 (bytesとbytearrayに%-formttingを追加)が同年3月に受理されたのを機に取り下げとなり、PEP-460がPython 3.5に含まれることになった。

これをもって2014年7月26日にこの案件は"won't fix"でcloseする。

これで決着したように見えたが、今年5月になってGregory P. Smithが、やっぱりbytes.format()が欲しいよ、というコメントをポストする。mini-languageはformatの方が強力なので、それが便利だという。

それに答えて6月10日のNick Coghlanのコメントは要約するとこういうことを言ってる。

  • str.formatの威力は拡張性のある__format__プロトコルformat組み込み関数によるところが大きく、これらが文字列操作に特化した実装になっているため、bytesのサポートは難しい。
  • %-formattingに対する論調は当初から変化している。最初「%はいずれ廃止されるのでformatを使ってくれ」だったのが「両方ともフルにサポートされている」に変わり、さらに今では「文字列にはformat、バイナリには%」になった。
  • 当初の「%を使うのはやめて」というガイダンスに従った人たちには申し訳ないが、また%に戻ってもらう必要がある。

というわけで、bytes.format()にはならなかったけど、bytes用の%-formattingが復活するという決着になった。
もっとも実際のところ現在使われているPython 3は3.4で、これにはbytes.formatもbytes.__mod__もないのだから、Python 3.4をしばらくサポートせねばならない状況下では3.5でbytes.__mod__が導入されても、当初の要件であったPython 2/3両互換はそれほど楽にはならない。正直遅きに失したという感じである。

Python 3.5の%-formattingはbytesのinterpolationに%bunicode文字列(str)のinterpolationには%aを使う。Python 2との互換性のために%s, %rもサポートされている。%aはオブジェクトに対してはrepr(object).encode('ascii')と同等で、オブジェクトが__bytes__を実装していれば、直接のinterpolationも可能。

途中で出てきたbytes.format()のために__bformat__を追加するという案よりは、自然な形に落ち着いたのかな、という印象だ。

もっと詳しい情報

Let's Note CF-S51にDebian 8.2.0を入れる

前提条件

  • HDDは20GBのものにアップグレードされている
  • かなり古いDebianが入っていて、一応ちゃんと起動する (ただしGrubは0.97)
  • 付属のフロッピードライブはあるが、フロッピーディスクがないし、面倒なのでフロッピーから起動するのはイヤ
  • BIOSはUSBからのブートをサポートしていない

準備

  • Debian 8.2.0のi386用netinst ISOイメージをダウンロードしておく

トライ1: GRUB経由でUSB stickからブートする

(あとで気がついたが、ここでethernetfirmware、e100/d101m_ucode.binをstickのfirmwareディレクトリにコピーしておくとあとで手間が省ける)

  • Let's NoteにUSB stickを挿して起動、Grubの画面が出たところでCを押しコマンドラインに移行、root ( とタイプしてTABを押す。

しかしhd0 と fd0 しか表示されない。USB stickを認識せず敗退。

トライ2: swap partitionからブート

  • Let's Noteを古いDebianで起動
  • swapoffでswapの使用を停止
  • トライ1のためにunetbootinで作ったUSB stickのpartition 1をddでswap partitionにコピーする
  • fdiskでboot可能フラグをつけ、partition typeをb (FAT32)に変える
  • reboot
  • Grubの画面が出たところでCを押しコマンドラインに移行、root (hd0,4) [ENTER] kernel /ubnkern [ENTER] initrd /ubninit [ENTER] boot [ENTER]

これでインストーラが起動する。

ただしインストール中にちょっと問題発生:

  • partition 5をCD-ROMとしてマウントできていないようなので、裏口(Alt-2)からmount /dev/sdb1 /cdromとしてマウントしてやる
  • インストールには物理メモリだけでは足りないので、swapは有効にしなければならない。swapパーティションにコピーしたブートイメージが壊されるので万一インストールが失敗した場合はコピーし直しになるが仕方ない(その場合はLinuxも起動しなくなるので、やっかいなことになるのだが)。

これで一応うまくいった。

Hatena Blogに移動

これまで頻度低いながらもHatena Diaryを使っていたが、ハテナ記法の数式は悲しいとこぼしたところ、Hatena BlogではMathJaxが簡単に使えるようになってますよ〜というコメントをいただいたので、移ることにした。2005年8月20日にはてなに移動という記事を書いているのが笑える。丁度10年ではないか。

実際こうやって最初の日記(「記事」とか呼ぶのはためらわれる)を書いていると、Hatena Diaryはその日のランダムな話題をつらつら書くのには適していたんだな、ということがわかる。Blogはタイトルがあって長い記事がジャジャーン、という感じで重たい。いろいろ考えてもあまり深く掘り下げることなく他のネタに移っちゃうタイプにはHatena Blogの形の方が合っている。