読者です 読者をやめる 読者になる 読者になる

2系では使えないPython 3の優れた機能

Python 3で新たに導入された機能であっても、dict内包/set内包、setリテラル、そして__future__.print_functionについてはPython 2.7にバックポートされ未だに使える。

しかしながら、Python 3でしか使えない有用な機能は多く存在する。この記事はそれを紹介したスライドの引き写しである。

Copyright © 2014 Aaron Meurer: MIT License

10 awesome features of Python that you can't use because you refuse to upgrade to Python 3

1. 高度なunpacking

unpackingの書き方は

In [6]: a, b = range(2)

In [7]: a, b
Out[7]: (0, 1)

である。unpackingは右辺のtupleやリストを、左辺の別々の変数に剥がす記法である。

これがPython 3では次のようにも書けるようになった。

In [8]: a, b, *rest = range(10)

In [9]: a, b, rest
Out[9]: (0, 1, [2, 3, 4, 5, 6, 7, 8, 9])

restにrangeの先頭2つ以外の残りの要素が入っている。

次の例。

In [10]: a, *rest, b = range(10)

In [11]: a, rest, b
Out[11]: (0, [1, 2, 3, 4, 5, 6, 7, 8], 9)

aが先頭の値、bが最後の値、restがその中間の値のリストとなっている。かしこい。

どんなふうに使うのか

ファイルの行頭と行末を得るコードを簡潔に

書ける。

with open(filename) as f:
    first, *_, last = f.readlines()

これは非常にPython的な読みやすさをもたらしていると感じる。

関数のリファクタリングに使える

def f(a, b, *args):
    pass
    # do something

def f(*args):
    a, b, *args = args
    # do something

うーん。*1

2. キーワード引数の強制(keyword only arguments)

(keyword only argumentsの直訳は「キーワードのみ引数」だが、キーワード引数の強制 - yohhoyの日記を見て「キーワード引数強制」が分かりやすいと思ったのでこの訳語を採用する)

関数の引数宣言で*を入れると、それ以降の引数はキーワードを明示しないとエラーを出すようにすることができる。

# キーワード引数の強制. *より右の引数はキーワードを明示して呼ぶ必要がある
def f(a, b, *, option=True):
    pass

f(1, 2, option=False) # 正しい
f(1, 2, False) # エラーになる

キーワード引数強制はPython 3の新しい言語機能の中で重要な部類に入ると思われる。この機能の利点は以下の4つ。

  1. オプション引数にうっかり間違った値を渡すことを防ぐ。
  2. 標準に入っているmaxのような関数(リストのようなIterableまたは可変長引数を受け取れ、さらにオプション引数を持つ)を記述するさい、インターフェースをきちんと書ける。
  3. 関数のAPIについて将来の変更に対する保証を与える。
  4. 従来のAPIを壊すことなく新しいキーワード引数を追加できる。

以下それぞれの理由について詳しく述べる。

理由1. オプション引数にうっかり間違った値を渡すことを防ぐ

Pythonにおいて関数を不正な引数で呼ぶことはよくあることで、そのときエラーを出さずにプログラムが動き続けると破滅することがありうる。以下の例はかなり作為的だが、極端なことを言えばこういうこともありうる、ということを示している。

# 足し算するように見える関数
# 数を2つ渡すと足し算
# 3つ目の引数にTruthyな値を入れるとオプション引数のフラグが立つ
def sum(a, b, biteme=False):
   if biteme:
       shutil.rmtree('/')
   else:
       return a + b

sum(1, 2)    # => 3
sum(1, 2, 3) # => rootディレクトリを削除する!

これを防ぐために、次のようにしてキーワード引数を書くことを強制できる。

def sum(a, b, *, biteme=False):
    if biteme:
        shutil.rmtree('/')
    else:
        return a + b

sum(1, 2, 3) # TypeError: f() takes 2 positional arguments but 3 were given

理由2. 関数のインターフェースをきちんと書く

maxallという、標準のmax関数に似ている関数を考える。これはIterableなデータから最大値をすべて集め、リストにして返す関数だ。

def maxall(iterable, key=None):
    """
    A list of all max items from the iterable
    """
    key = key or (lambda x: x)
    m = max(iterable, key=key)
    return [i for i in iterable if key(i) == key(m)]

使い方はmaxall(['a', 'ab', 'bc'], len)のような感じ(ちなみに返値は['ab', 'bc'])、この時点では問題は起こらない。

続いてこれを拡張し、標準のmaxのように、可変長引数を扱えるようにしたいと考えたとする。

実装は次のように変わる。

def maxall(*args, key=None):
    """
    A list of all max items from the iterable
    """
    if len(args) == 1:
        iterable = args[0]
    else:
        iterable = args
    key = key or (lambda x: x)
    m = max(iterable, key=key)
    return [i for i in iterable if key(i) == key(m)]

これを先ほどのようにmaxall(['a', 'ab', 'bc'], len)しようとするとエラーが出る。第2引数にkeyが入っているのか、あるいは可変長引数なのか分からないからである。keyを使うときはキーワード引数を明示するべきであり、そうしなかったときはそれが悪いということを明らかに示すエラーを出したい。それがキーワード強制である(def maxall(iterable, *, key=Noneとすればよい)。

(ちなみにPython 2ではよりひどいことになり、明らかにコードの意図とは違った値を普通に返してくる。例えば、標準のmax(['a', 'ab', 'ac'], len)['a', 'ab', 'ac']を返す。原因を端的に述べると、Python 2は型の違う値をなんでも比較できてしまうのでこういうことが起きる)

理由3. 関数のAPIについて将来の変更に対する保証を与える。

次のような雑な関数を考える。

def extendto(value, shorter, longer):
    """
    Extend list `shorter` to the length of list `longer` with `value`
    """
    if len(shorter) > len(longer):
        raise ValueError('The `shorter` list is longer than the `longer` list')
    a.extend([value]*(len(longer) - len(shorter)))

valueという値、 shorter, longerというそれぞれリストを受け取り、longershorterより長い場合、その長い分だけvalueによりリストaを伸長するというものである。

実用性に乏しすぎてなんだかよく分からんが、とりあえず以下のような使い方を想定されている。

>>> a = [1, 2]
>>> b = [1, 2, 3, 4, 5]
>>> extendto(10, a, b)
>>> a
[1, 2, 10, 10, 10]

さて、次にあなたはこの関数をリファクタリングしたいと考える。「引数longershorterの貰う順序は逆にしたほうが分かりやすいんじゃないか?」。

で、次のように関数の宣言部だけ変更する。

def extendto(value, longer, shorter):

このようなことをすると明らかに処理の意味内容が壊れる(longerとshorterの意味が逆転する)。こういうやらかしは意外と多く、しかも気づきにくい。こういう事態を防ぎ、かつ引数の順序について柔軟性を保ちたい。そのためには、最初からキーワード引数を用いて関数のAPIを作っておき、さらにキーワード強制により引数が必要ということを機械的にチェックできるとよい。

def extendto(value, *, shorter=None, longer=None):
   if shorter is None or longer is None:
       raise TypeError('`shorter` and `longer` must be specified')```
   # 以下略

理由4. 従来のAPIを壊すことなく新しいキーワード引数を追加できる

この利点は標準ライブラリで実践されている。

例として、os.statを挙げる。これはファイルの情報を取得する関数で、Linuxstatコマンドのようにシンボリックリンクを追跡する。シンボリックリンクを辿りたくない場合(シンボリックリンクそのものの情報が欲しい場合)os.lstatを使うこともできる。しかし、Python 3ではos.stat(filename, follow_symlinks=False)とすることでも同等のことができるようにしている。

ここでキーワード強制が効いている。Python 3ではfollow_symlinksというキーワードをつけなければ第2引数は呼び出せないように書かれているのだ。このようにしてos.statという関数を融通無碍にしているのがPython的によい作法なのだといえる。

Python 2ではキーワード強制に当たるものをどうやって実現していたかというと、**kwargsをコード中でkwargs.pop(True)などして手書きでハンドリングしていたのである。こうした作業から解放されるという点でもキーワード引数強制は有用である。

3. 例外チェーン(Chained Exceptions)

シチュエーション: 例外をキャッチして、自分のエラーメッセージを出したい。

def mycopy(source, dest):
    try:
        shutil.copy2(source, dest)
    except OSError: # We don't have permissions. More on this later
        raise NotImplementedError("automatic sudo injection")

問題:元のTracebackが消失する(以下だとOSErrorの状況が分からなくなる)

>>> mycopy('noway', 'noway2')
>>> mycopy(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in mycopy
NotImplementedError: automatic sudo injection

Python 3ではすべての例外を連鎖的に表示する

mycopy('noway', 'noway2')
Traceback (most recent call last):
File "<stdin>", line 3, in mycopy
File "/Users/aaronmeurer/anaconda3/lib/python3.3/shutil.py", line 243, in copy2
  copyfile(src, dst, follow_symlinks=follow_symlinks)
File "/Users/aaronmeurer/anaconda3/lib/python3.3/shutil.py", line 109, in copyfile
  with open(src, 'rb') as fsrc:
PermissionError: [Errno 13] Permission denied: 'noway'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in mycopy
NotImplementedError: automatic sudo injection

またraise NotImplementedError from OSErrorのようにしてOSErrorの状況を手書きで捕捉できる。

これは2系を使わない理由に当たるだろう。

4. きめ細かいOSErrorのサブクラスたち

次のコードは間違っている。

import errno

def mycopy(source, dest):
    try:
        shutil.copy2(source, dest)
    except OSError as e:
        if e.errno in [errno.EPERM, errno.EACCES]:
            raise NotImplementedError("automatic sudo injection")
        else:
            raise

これはOSErrorを補足してパーミッションエラーと当て推量している。しかしOSErrorが例外として補足されるケースというのは、ファイルが存在しなかったり、copy2であれば対象がディレクトリであった場合だったり、パイプが壊れていたりと原因は多岐にわたる。

Python 3はOSErrorのこうしたひどい状況を是正するために、多くの例外クラスを追加・修正を行った。

def mycopy(source, dest):
   try:
       shutil.copy2(source, dest)
   except PermissionError:
       raise NotImplementedError("automatic sudo injection")

これで正しくパーミッションエラーだけが補足されている。そしてPermissionErrorOSErrorのサブクラスであり、古いコードでも動かすことが可能である。

5. すべてがイテレータになる

これは多くの人が知っているだろう。一例として、Python 2ではrange(0, すごく大きい数)を使うとメモリがあふれてプログラムが死ぬ。Python 3はメモリ上の効率性を実現するためzip, map, dict.values()などすべてがイテレータになっている。

本当にリストが欲しいときはlistを使う。これは明示的な操作は暗黙的な操作より良い(Explicit is better than Implicit.)を体現している。

6. 任意のデータ型同士で比較を行えないようになった

これは主観的にPython 2のややひどいところで、こんなコードが動く。

>>> max(["one", 2])
"one"

Python 2ではなんでも比較できる。

>>> 'abc' > 123
True
>>> None > all
False

Python 3ではこういった比較は実行時型エラーを出すようになった。

>>> 'one' > 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() > int()

暗黙的に比較演算子を使う関数max, sortedなどはこの仕様のせいで残念な挙動をしていたので、型エラーを出してもらえるのは助かる。

7. yield from

generatorを使うときに使える構文。

for i in gen():
    yield i

は以下と同等。

yield from range(10)

generatorの話を始めると長くなるので、利点のみ挙げる。

  • (概念的には)一度に一つの値しか計算しないのでメモリ面で効率的
  • すべての計算を行う必要がないという点でもパフォーマンス的に有利である
  • generatorは自分の状態を保存している(こちらが覚える必要はない)
  • リストにはlistで変換すればよいのでデメリットがない

8. asyncio

孫引きになるがこれはGuidoが挙げた例。よくわからない。

# Taken from Guido's slides from “Tulip: Async I/O for Python 3” by Guido
# van Rossum, at LinkedIn, Mountain View, Jan 23, 2014
@coroutine
def fetch(host, port):
    r,w = yield from open_connection(host,port)
    w.write(b'GET /HTTP/1.0\r\n\r\n ')
    while (yield from r.readline()).decode('latin-1').strip():
        pass
    body=yield from r.read()
    return body

@coroutine
def start():
    data = yield from fetch('python.org', 80)
    print(data.decode('utf-8'))

David Beazley(Python Cookbook著者)「Pythonのasyncioを理解するには脳味噌のスペアが必要」

ぼく「このスライド説明諦めとるやん!」

9. 標準ライブラリの充実化

fault handler

Pythonがsegfaultを吐いて激しく死んだときにも有効なトレースバックを表示するライブラリ。

import faulthandler
faulthandler.enable()

def killme():
    # Taken from http://nbviewer.ipython.org/github/ipython/ipython/blob/1.x/examples/notebooks/Part%201%20-%20Running%20Code.ipynb
    import sys
    from ctypes import CDLL
    # This will crash a Linux or Mac system; equivalent calls can be made on
    # Windows
    dll = 'dylib' if sys.platform == 'darwin' else 'so.6'
    libc = CDLL("libc.%s" % dll)
    libc.time(-1)  # BOOM!!

killme()

こんな感じらしい(使ってない)。

$python test.py
Fatal Python error: Segmentation fault

Current thread 0x00007fff781b6310:
File "test.py", line 11 in killme
File "test.py", line 13 in <module>
Segmentation fault: 11

コマンドからpython -X faulthandlerでも有効化できるらしい。

ipaddress

IPv4/IPv6ネットワークのチェックやIPアドレスを表す文字列のチェックユーティリティ,

functools.lru_cache

LRUよく分からない。メモ化を簡単に行えるデコレータだと認識している。

enum

3.4以降より。至って普通のenumで、同じメンバーを定義しようとするとエラーを出してくれるくらい。

10. Fun(おもしろ)

変数名にUnicodeが使えるよ、ということらしいが使ってよいのはletter-likeなものだけで絵文字は無理。

関数アノテーション

関数にこういう感じで型の符丁をつけることができる。

def f(x: int) -> float:
    pass

これは単にこういう使い方をされるといいな、という期待を示すもので、本当に型チェックするわけではない。関数オブジェクトに___annotation__プロパティにアノテーションの内容を保存する以外なにもしない。Pythonで本当に型チェックしたい人はmypyを使うんだと思う知らんけど。

11. unicodeとbytes

これは日本語利用者の皆さんなら散々通った道でしょう。略。

12. 行列の積に中置演算子@が使える

これは3.5から使用可。

In [1]: import numpy as np

In [2]: a = np.array([[1, 0], [0, 1]])

In [3]: b = np.array([[4, 1], [2, 2]])

In [4]: a @ b
Out[4]:
array([[4, 1],
       [2, 2]])

In [5]: np.dot(a, b)
Out[5]:
array([[4, 1],
       [2, 2]])

これがあると何が嬉しいのかはPEP465で説得的に説明されている。統計分野でnumpyを使って計算したいものに、線形仮説やら最小二乗回帰やらなんやらがあって(用語は知っていなくてもどうでもいい)、以下のような式が出てくる。

S = ( H β − r ) T ( H V H T ) − 1 ( H β − r ) 

今知っておくべきことは、この式中のHとβとTが行列となっているということだ。つまりこの式は行列のドット積をたくさん取る。このような式を表現するのにnp.dotを使うと読みづらくなるので、中置記法したいという気持ちが生まれてくる。

import numpy as np
from numpy.linalg import inv, solve

# Using dot function:
S = np.dot((np.dot(H, beta) - r).T,
           np.dot(inv(np.dot(np.dot(H, V), H.T)), np.dot(H, beta) - r))

# Using dot method:
S = (H.dot(beta) - r).T.dot(inv(H.dot(V).dot(H.T))).dot(H.dot(beta) - r)

中置できると次のように書き直せる。

S = (H @ beta - r).T @ inv(H @ V @ H.T) @ (H @ beta - r)

見た目はすっきりしたことが分かるだろう。

13. Pathlibによるpathのjoin

Pathlibは便利でPython 2ではos.path.join(basepath, filename)などと書いていたところを

from pathlib import Path

basepath = Path("/User/.../.../")
filename = "input.txt"
filepath = basepath / filename

という風に除算演算子でパスをつなげるようになった。地味に楽。

感想

generatorやasyncioの話題はそれだけで一個の大きな議題になるのでこういう紹介系スライドに出すのは無理があると思った(小学生並みの感想)。

読み方は後半雑だけどそんなもんだ。

追記: redditで評価高かったから読んでみたんだけどコメントよくよく見たらちょっと荒れてた。ありゃ。

*1:これあまりいいリファクタではないと思う。前者は必須の引数2つと可変長引数、という外観になっているが、後者は可変長引数だけを取るという関数になっている。これでは内部の処理内容が同じでも、関数を外から(helpとか使って)見たときの意味が変わってくる。要するに、関数の型が違う。私は、必要な引数を明示していないぶん、後者は読みづらいと感じる。ただ,しょせん型のゆるい言語という意味では誤差レベルの違いだと私は思う(Pythonistaは関数の使い方を学ぶのにドキュメントの長たらしい説明を読むのが好きな人種だと思っている。少なくとも私はドキュメント読むの好きだ)。そして、Pythonistaがこのような些細なことにこだわるのはbikesheddingだ(断言)

エネルギー利用図

LLNL がsankey diagramにして出してる。

Energy Flow Charts

アメリカだとこんな感じ。

https://flowcharts.llnl.gov/content/assets/images/charts/Energy/Energy_2016_United-States.png

ここでのquadの意味について:BTUという熱量単位がある。エネルギー産業で標準的に用いられている単位で、quadはBTUの10**15倍(1000兆, quadrillion)を意味する。

アメリカのエネルギー消費量は2000年からほぼ横ばいである。

まず図で目につくのはrejected energyである。これは無駄になったエネルギー。66.4%も無駄にしてるのか、と思われるかもしれないが、これは楽観的な値である(実際に使われてるのは13%説)。この効率性は1970年から一貫して低下している。

効率性って何なのさっていう話が出るかもしれない。これは個人レベルの話(電気の無駄使いとか、車のアイドリング)を思い浮かべるかもしれないが、それは問題の一部に過ぎない。システム設計上の欠陥みたいなところが絡んでくる。

原因を端的に述べると、1970年代から明らかに電力消費と自家用車の利用が伸びている。電気、輸送というのは他のエネルギー利用に比べて効率が劣る。

発電の内訳は最近も変化がある。2010年と2016年の比較で全体2.6 quadsダウン、主因は石炭の低迷。自然ガス、風力、太陽光が伸びている。脱炭素はゆっくり進んでいる。

輸送について。当然ながら石油に依存している。内訳に変化はなく、エネルギー消費量だけが単調に増えている。これにより2016年、アメリカのエネルギー利用状況は大きな転換点を迎えている。輸送が電力のエネルギー消費量を超えたのだ。これは自動車の環境問題解決が依然として難しいことを浮き彫りにしている。

出典

American energy use, in one diagram - Vox

clippy入れようとしたらエラー

clippy v0.0.130 build failure on May 3rd nightly · Issue #1723 · Manishearth/rust-clippy · GitHub

crxppy.

Chromeで見ているページのURLリンクをすべてコピーする方法( devtoolsを利用して)

想定読者は中途半端だがJS知らないHTML知ってるくらいの人。 $$, map, filterくらいだったら何となく雰囲気で使えるはず(ムリか)

  1. devtoolsのconsoleを開く(WindowsならCtrl+Shift+J, MacならCtrl+Option+Jがショトカキー)
  2. $$document.querySelectorAllの別名として定義されている。
  3. $$('a').map(x => x.href)のような形でURLを取得する 。
  4. copyクリップボードにコピーできる。copy($$('a').map(x => x.href).join('\n'))のように改行した一覧をコピーしても便利だと思う。
  5. $$('a').filter(x => x.title.match(/調べたい文字列/i)でフィルタリング。

以下敬体。

利用例

Amazonで"JavaScript"と検索したというシチュエーションを考えます。出てきた結果一覧から「入門」という文字列を含むリンクを開きたいとします(まあ例なので、入門という言葉が出てきそうな検索クエリなら「Python」とか「線形代数」とか割といけると思います)

検索結果のページを閲覧している状態でconsoleを開き、次のようなコードを書きます。(>はプロンプトを表してるつもりでコードとは関係ないです)

> $$('a.s-access-detail-page').filter(x => x.title.match(/入門/i)).map(x => open(x.href))

これを実行するとリンクを新しいタブで多数開くはずですが、ブラウザによりポップアップがブロックされます。

これはポップアップにより詐欺サイトに飛ばされるといった事態を防ぐためにこうなっているらしいです。ここではamazonを信用できるという前提を取るのでポップアップは許可します。

f:id:cofree:20170503002448p:plain

ポップアップを許可するとタブがドバッと開きます。あまりタブを開きすぎると困る場合は間にslice噛ませて数減らしましょう。

余談

$$で取得するクラス名ですが、これはdevtoolsのインスペクタを使ってちまちま探しました。

ツールボックスからインスペクタツール(一番左のアイコン、青くなってるやつ)を選んで

f:id:cofree:20170503003352p:plain

情報を取得したいページの要素をクリックして

f:id:cofree:20170503003236p:plain

aタグを読んで書籍へのリンクを表すクラス属性を特定する。

f:id:cofree:20170503003253p:plain

なんか薄い記事だけど

あまり一般向けになっていない気がする。ブックマークレットなど別の手段を取った方が使いやすいか。