mypy で静的型付け Python プログラミング

Pythonへの型アノテーション導入を目指すGuido van Rossum氏

mypy の型アノテーションの構文を Python に取り込もうとメーリングリストで提案したニュースが話題になりました。今夏に話題になったニュースを今冬にフォローするという季節外れ感ですが、ずっと興味はあって「あとで読む」状態になっていたのを調べてみました。

基本的には mypy のドキュメントから自分の興味のあるところをかいつまんで説明しています。分かりにくいところがあれば原文を参照してください。

Welcome to Mypy documentation!

mypy とは

mypyPython の静的型チェッカー (static type checker) です。wikipedia:静的コード解析wikipedia:en:Lint (software) ツールの一種だと説明されています。

Python2 にはなく、Python3 で PEP 3107 - Function Annotations が追加されました。Python 3.0 がリリースされたのが 2008-12-03 なので6年ぐらい前のことです。

>>> def f(i: int, s: str) -> None: pass
>>> f.__annotations__
{'s': <class 'str'>, 'return': None, 'i': <class 'int'>}

但し、これは情報として保持しているだけで関数アノテーションをどう活用するかはライブラリやサードパーティツールに委ねられていました。mypy はそんなツールの1つであり、先のコードのように関数アノテーションに型シグネチャを記述することで静的解析に利用しようといったものです。重要な点の1つとして、関数アノテーションは実行時に影響を及ぼさないため、仮にこの型シグネチャの定義が間違っていたとしても何も起こりません。

さらに関数定義だけでなく、コレクションや変数定義にも型シグネチャを付加する仕組みがあり、それらをまとめて型アノテーション (type annotation) と呼ぶようです。Python 標準の組み込み型だけでは型シグネチャを記述できないため、Built-in types に mypy で定義された組み込み型が紹介されています。

実際に mypy の型アノテーションの宣言を見てみましょう。

>>> from typing import Dict, List
>>> l = List[int]()       # Create empty list with type List[int]
>>> l
[]
>>> d = Dict[str, int]()  # Create empty dictionary (str -> int)
>>> d
{}

これは空のリストとディクショナリの型アノテーションを宣言しています。型アノテーションを定義しつつも実際に空のリストが作成されています。typing.py に型アノテーションのための型定義があるので少し覗いてみます。

class TypeAlias:
    """Class for defining generic aliases for library types."""

    def __init__(self, target_type):
        self.target_type = target_type

    def __getitem__(self, typeargs):
        return self.target_type

List = TypeAlias(list)
...

TypeAlias に組み込み型を渡して self[key] で評価されたときにその組み込み型を返します。

>>> List is list
False
>>> List[int] is list
True

シンプル且つ実際に動く Python のコードな、うまい仕組みですね。

Python3 への導入

Guido 自ら提案したせいか、メーリングリストで多くの議論が行われたようです。興味がある方は以下のメールのスレッドを追いかけてみると良いと思います。私はそこまで根気がなくて発端となった Guido の提案メールのみを読んでみました。

前者のメールの要約は冒頭に紹介した InfoQ の内容なのでそれを参照してください。後者のメールは、その提案に対する反応が3つの質問に分類されるとみなし、それぞれの質問に対する Guido の回答のようです。いくつか要点を抜き出して意訳してみます。

(A) 選択的静的型付け (Optional static typing) の標準化は本当に必要なのか?

多くの人が肯定的であり、数人は不要だと判断しているが、反対している人の大半はその代替となる自分の提案があるようにみえる。確信はないのだけれど、自分の直感で言うと、できるだけこれはオプションにしておきたい。どんなケースでも、それが本当に価値があるものかどうかは実際に作り始めるまでは分からないし、これは大丈夫だろうと信じて推進するしかない。前提としていることを1つ付け加えると、主な用途は lint 的なこと、IDE、ドキュメント生成になるだろう。これらに1つ共通して言えることは、型チェックが失敗したとしてもプログラムは実行できるし、型を追加することがそのパフォーマンスを下げないということだ (上げることもないが :-) 。

(B) 選択的静的型付け (Optional static typing) の標準としてどんな構文にするか?

たくさんの興味深い質問がみられた。実現方法として、3つか4つの "families" があって、まずやることはその系統を選択することだ。

1. mypy family
関数アノテーションの特徴を活かし、Python の構文としてもそのまま有効だ。標準ライブラリや C 拡張のアノテーションを集約する "stub" ファイルを別に設けることもできる。mypy のアノテーションが (stub ファイルではなく) インラインに記述されると、アノテートされた関数本体の型チェクと同様に呼び出し側の入力の型チェックにも使われる。

2. pytypedecl family
独立した stub ファイルでカスタム構文を使うため、Python の構文に制約を受けず、若干 mypy よりは洗練されているようにみえる。

3. PyCharm family
docstings でのみ使われるカスタム構文である。stub ファイルを使うこともできる。

4. コメントに全て書く方法を4番目の系統になり得るが、誰もが真面目にコメントを書くとは思えないし、利点も分からない。

さぁ、どうやって選択しよう?私は攻撃的にも防御的にもそれぞれのアプローチで熱心に内容を読み取った。3つのプロジェクトは異なる成熟期にある (おもちゃ以上、プライムタイムには及ばない) というのが実感だ。特定の型システムの機能 (前方参照、総称型、ダックタイピング) に関しては、私は全てに満足できるものを期待していて、おそらくはまだやることがある。どのプロジェクトも stub をサポートしているので、既存のコードを修正することなくシグネチャを指定できる。

私にとって、間違いなく mypy が最も Pythonic なアプローチだ。我々が PEP 3107 (function annotations) を議論したとき、最終的に型アノテーションのために使われるのがずっと私の目標だった。当時は型チェックのルールになるといった合意はなかったが、構文上の位置付けからそれを疑う余地はなかった。そのため、我々は、サードパーティの実験の成果が最終的に満足できるものを創り出すのを願って Python3 に "annotation" を導入することを決定した。mypy はそういった実験の1つだ。mypy から私が得た大事な教訓の1つは、型アノテーションは linter に対して最も有益であり、(通常は) 実行時に型を強制するために使われるべきではないということだ。またそれらはコード生成の役には立たない。我々が PEP 3107 で議論していたときには全く分からなかったことだ!

(中略)

ということで、私は mypy family を選択していて、mypy の改善についての議論に移っていこう。そして誰かが pytypedel や PyCharm stub から mypy stub への変換ツールを作ってくれて、これらの2つのプロジェクトの stub 定義を再利用できることを願う。無論、PyCharm や pytypedecl が mypy の構文を導入することも願っている (まずはネイティブの構文に追加して、最終的には1つの構文になる) 。

(C) 他の機能にも対応する/すべき?
(追伸) この質問についてはあまり議論しなかったことに気付いた。わざとだ。特定の mypy の機能については別スレッドでこれから議論しよう (このスレッドでもいいけど :-) 。

Python 3.5 のリリーススケジュールは以下になります。feature freeze は Beta1 (May 24, 2015) のようです。

ちょうど型アノテーションの PEP のドラフト (のドラフト?) が Guido からメーリングリストに投稿されました。

アノテーションと型システムの議論

mypy について調べていて見つけた記事などのリンクです。

私には難しくてまとめきれないため、Guido が参照している漸進的型付け (Gradual Typing) も含め、また別の機会に、、、。

追記: 以下にまとめました。

mypy を使ってみよう

さて、ここからが本題です。

mypy のインストール

PyPI にも登録されていますが、ここでは github からソースをクローンしてインストールします。現時点では、mypy は Python 3.2 以上しかサポートしていませんが、 Python2 対応も開発中だと FAQ にあるのでいずれサポートされるかもしれません。

$ mkvirtualenv -p /opt/local/bin/python3.4 mypy
(mypy)$ git clone git@github.com:JukkaL/mypy.git
(mypy)$ cd mypy/
(mypy)$ python setup.py install
(mypy)$ mypy -h
usage: mypy [option ...] [-m mod | file]

Optional arguments:
  -h, --help         print this help message and exit
  --html-report dir  generate a HTML report of type precision under dir/
  -m mod             type check module
  --verbose          more verbose messages
  --use-python-path  search for modules in sys.path of running Python

Environment variables:
  MYPYPATH     additional module search path

ライブラリーのスタブ

Python の標準ライブラリに含まれるモジュールを使うコードの型チェックを行うには、public なインターフェイスやクラス、変数、関数などのスケルトンをスタブとして定義しないといけません。どんな雰囲気か mypy/stubs at master · JukkaL/mypy · GitHub にあるのを見た方が手っ取り早いと思います。作成したスタブの場所は環境変数で指定できるようです。

$ export MYPYPATH=~/work/myproject/stubs

チュートリアル

関数アノテーションに型アノテーションを指定する簡単な例から見てましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 import typing                                                                    
  3                                                                                  
  4 def greeting1(name: str) -> None:                                                
  5     return 'Hello, {}'.format(name)                                              
  6                                                                                  
  7 def greeting2(name: str) -> int:                                                 
  8     return 'Hello, {}'.format(name)                                              
  9                                                                                  
 10 def greeting3(name: str) -> str:                                                 
 11     return 'Hello, {}'.format(name)                                              
 12                                                                                  
 13 print(greeting1('john'))                                                         
 14 print(greeting2('bob'))                                                          
 15 print(greeting3('mike'))    

このプログラムは mypy の機能を使っていない純粋な Python3 のスニペットです。

(mypy)$ python check_function_signature.py 
Hello, john
Hello, bob
Hello, mike

普通に実行できました。しかし、せっかく指定した関数アノテーションの返り値が間違っているものがありますね。これを mypy というコマンドラインツールで実行すると lint 的に静的解析してくれます。

(mypy)$ mypy check_function_signature.py 
check_function_signature.py: In function "greeting1":
check_function_signature.py, line 5: No return value expected
check_function_signature.py: In function "greeting2":
check_function_signature.py, line 8: Incompatible return value type: expected builtins.int, got builtins.str
check_function_signature.py: At top level:
check_function_signature.py, line 13: "greeting1" does not return a value

mypy が静的型チェッカーだというのは、Python のプログラムをインタープリターを実行する前に、自分で mypy ツールを実行して型チェックを行い、そのエラーを修正するといった使用方法だからです。

次の例をみてみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Iterable                                                      
  3                                                                                  
  4 def greet_all1(names: Iterable[str]):                                            
  5     for name in names:                                                           
  6         print('Hello, {}'.format(name))                                          
  7         'mr. ' + name                                                            
  8         name + 1                                                                 
  9                                                                                  
 10 greet_all1(['john', 'bob'])                         

こんどは Iterable を使って型アノテーションを定義しています。

(mypy)$ mypy check_function_signature_with_error.py 
check_function_signature_with_error.py: In function "greet_all1":
check_function_signature_with_error.py, line 8: Unsupported operand types for + ("str" and "int")

おや!?8行目に str 型と int 型の加算があります。これを Python インタープリターで実行すると、

(mypy)$ python check_function_signature_with_error.py 
Hello, john
Traceback (most recent call last):
  File "check_function_signature_with_error.py", line 10, in <module>
    greet_all1(['john', 'bob'])
  File "check_function_signature_with_error.py", line 8, in greet_all1
    name + 1
TypeError: Can't convert 'int' object to str implicitly

エラーになりますね。とても単純な例ですが、1 というリテラルの値が int 型だと型推論されて mypy がエラーを指摘しています。

もっと分かりやすい型推論のサンプルも見てみましょう。

  1 # -*- coding: utf-8 -*-                                                                         
  2 import typing                                                                                   
  3                                                                                                 
  4 # int                                                                                           
  5 i = 1                                                                                           
  6 i = 2                                                                                           
  7 i = int(3)                                                                                      
  8 i = 'x'                                                                                         
  9                                                                                                 
 10 # list                                                                                          
 11 l = [1, 2]                                                                                      
 12 l.append(3)                                                                                     
 13 l.append('x')                                                                                   
 14 l.extend([4, 5])                                                                                
 15 l.extend([6, 'y'])                                                                              
 16                                                                                                 
 17 # dictionary                                                                                    
 18 d = {'x': 1}                                                                                    
 19 d['y'] = 2                                                                                      
 20 d['z'] = 'x'                                                                                    
 21 d[3] = 4  

mypy でチェックします。

(mypy)$ mypy check_type_inference.py 
check_type_inference.py, line 8: Incompatible types in assignment (expression has type "str", variable has type "int")
check_type_inference.py, line 13: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
check_type_inference.py, line 15: List item 2 has incompatible type "str"
check_type_inference.py, line 20: Incompatible types in assignment
check_type_inference.py, line 21: Invalid index type "int" for "dict"

変数へ最初に代入した型とあわない値を代入するとエラーとして指摘してくれます。

また型アノテーションをコメントで定義することもできます。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import List                                                          
  3                                                                                  
  4 l1 = []  # type: List[int]                                                        
  5 l1.append('x')                                                                    
  6                                                                                  
  7 l2 = List[int]()                                                                  
  8 l2.append('y')                               

# type: で始まるコメントを型アノテーションとして扱うため、

(mypy)$ mypy check_type_annotations.py 
check_type_annotations.py, line 5: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
check_type_annotations.py, line 8: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"

どちらの定義方法でも mypy では同様にエラーを指摘します。

最後に値の型を明示的に宣言する方法です。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Undefined                                                     
  3                                                                                  
  4 s = Undefined(str)                                                               
  5 if s:                                                                            
  6     print('not defined yet')                                                     
  7 s = 1                              

mypy でチェックすると、str 型の何かを期待しています。

(mypy)$ mypy check_explicit_types.py 
check_explicit_types.py, line 7: Incompatible types in assignment (expression has type "int", variable has type "str")

この Undefined はもう1つ仕掛けがあります。s に str 型の値が代入される前に操作を行うと例外が発生します。

(mypy)$ python check_explicit_types.py 
Traceback (most recent call last):
  File "check_explicit_types.py", line 5, in <module>
    if s:
  File "path/to/.virtualenvs/mypy/lib/python3.4/site-packages/typing.py", line 359, in __bool__
    raise TypeError("'Undefined' object is not valid as a boolean")
TypeError: 'Undefined' object is not valid as a boolean

ちょっと実装をみてみると、

class Undefined:
    def __repr__(self):
        return '<typing.Undefined>'

    def __setattr__(self, attr, value):
        raise AttributeError("'Undefined' object has no attribute '%s'" % attr)

    def __eq__(self, other):
        raise TypeError("'Undefined' object cannot be compared")

    def __call__(self, type):
        return self

    def __bool__(self):
        raise TypeError("'Undefined' object is not valid as a boolean")

Undefined = Undefined()

Undefined に対する操作 (特殊メソッド) を行うと例外を発生させています。if 文の場合は Python3 からは特種メソッド__bool__ に変更されています。Python2 のときは __nonzero__ でした。

静的型付けプログラミング

チュートリアルで静的型チェッカーとしての mypy の雰囲気は掴めました?ここではさらに mypy が提供する強力な型アノテーションをみていきましょう。

様々な型

ユーザー定義型
  1 # -*- coding: utf-8 -*-                                                          
  2 import typing                                                                    
  3                                                                                  
  4 class A:                                                                         
  5     def f(self) -> int:  # Type of self inferred (A)                             
  6         return 2                                                                 
  7                                                                                  
  8 class B(A):                                                                      
  9     def f(self) -> int:                                                          
 10          return 3                                                                
 11                                                                                  
 12     def g(self) -> int:                                                          
 13         return 4                                                                 
 14                                                                                  
 15 b = B()       # OK infer type B for b                                            
 16 print(b.g())                                                                     
 17                                                                                  
 18 a = B()       # type: A  # OK (explicit type for a; override type inference)     
 19 print(a.f())  # 3                                                                
 20 print(a.g())  # Type check error: A has no method g                             

18行目で型アノテーションで明示的に A と指定したら g メソッドがないと指摘されます。

(mypy)$ mypy check_user_defined_types.py 
check_user_defined_types.py, line 20: "A" has no attribute "g"

ちなみにこの型アノテーションの隣の実コメント部の # を取り除くと、

 18 a = B()       # type: A  OK (explicit type for a; override type inference)

パースエラーになりました。

(mypy)$ mypy check_user_defined_types.py 
check_user_defined_types.py, line 18: Parse error before "OK"

アノテーションとコメントは分割して書かないといけないようです。

任意の型を表す Any とキャスト

wikipedia:動的型付け したい値を定義するときは Any を使います。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Any, Undefined, cast                                                         
  3                                                                                                 
  4 x = Undefined(Any)                                                                              
  5 x = 1                                                                                           
  6 x = 'x'

このとき、普通の Python のコードのように mypy では静的チェックをしなくなります。

(mypy)$ mypy check_any_type.py 

さらに Any で定義した値をキャストすることもできます。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Any, Undefined, cast                                                         
  3                                                                                                 
  4 x = Undefined(Any)                                                                              
  5 x = 1                                                                                           
  6 x = 'x'                                                                                         
  7                                                                                                 
  8 y = cast(int, x)  # cast x as int to y                                                          
  9 y = 'y'                                                                                         
 10 y = 2                                                                            
 11                                                                                  
 12 z = Any(y)  # cast y as Any to z                                                 
 13 z = 3                                                                            
 14 z = 'z'                                           

今度は x を int でキャストしたものを y として定義し、y をまた Any にキャストして z として定義しています。

(mypy)$ mypy check_any_type.py 
check_any_type.py, line 9: Incompatible types in assignment (expression has type "str", variable has type "int")

Any(...)cast(Any, ...) は等価なようです。

Callable 型

組み込み型の Function を使います。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Function, List                                                               
  3                                                                                                 
  4 def label_data(name: str, data: List[int]) -> str:                                              
  5     return '{}: {}'.format(name, ', '.join(str(i) for i in data))                               
  6                                                                                                 
  7 def caller(name: str, data: List[int], func: Function[[str, List[int]], str]) -> str:           
  8     return func(name, data)                                                                     
  9                                                                                                 
 10 print(caller('numbers', [1, 2, 3], label_data))                                                 
 11 print(caller('mixed', [1, 'x', 3], label_data))                                        

構文は Function[[引数1の型, ..., 引数nの型], 返り値の型] なので複雑な関数だとちょっと苦しい感じはあります。

(mypy)$ mypy check_callable_types.py 
check_callable_types.py, line 11: List item 2 has incompatible type "str"

lambda もコンテキストから双方向に型推論されます。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Iterable, Undefined                                                          
  3                                                                                                 
  4 l1 = Undefined(Iterable[int])                                                                   
  5 l1 = map(lambda x: x + 1, [1, 2, 3])  # infer x as int and l as List[int]                       
  6                                                                                                 
  7 l2 = Undefined(Iterable[int])                                                                   
  8 l2 = map(lambda x: x + '+test', ['a', 'b'])                                                     
  9                                                                                                 
 10 l3 = Undefined(Iterable[int])                                                                   
 11 l3 = map(lambda x: str(x) + '+test', [1, 2])                                

あくまで例なので実際には lambda のような用途にわざわざ Undefined を定義するとは思いませんが、こういったコンテキストから凡ミスを防ぐのには良さそうにみえます。

(mypy)$ mypy check_lambda.py 
check_lambda.py, line 8: Incompatible return value type: expected builtins.int, got builtins.str
check_lambda.py, line 8: Argument 1 to "map" has incompatible type Function[[str], str]; expected Function[[str], int]
check_lambda.py, line 11: Incompatible return value type: expected builtins.int, got builtins.str
check_lambda.py, line 11: Argument 1 to "map" has incompatible type Function[[int], str]; expected Function[[int], int]

ダックタイピング

wikipedia:ダック・タイピング のコード例です。

  1 # -*- coding: utf-8 -*-                                                                         
  2 def quack(a: A) -> None:                                                                      
  3     print(a.f())                                                                                
  4                                                                                                 
  5 class A:                                                                                        
  6     def f(self) -> str:                                                                         
  7         return 'x'                                                                              
  8                                                                                                 
  9 quack(A())                                                                                      
 10                                                                                                 
 11 class B:                                                                                        
 12     def f(self) -> int:                                                                         
 13         return 1                                                                 
 14                                                                                  
 15 quack(B())                   

B は A と継承関係にないため、15行目の quack(B()) でエラーになります。

(mypy)$ mypy check_duck_typing.py 
check_duck_typing.py, line 15: Argument 1 to "quack" has incompatible type "B"; expected "A

ducktype というクラスデコレーターを使うことで回避できます。

 11 from typing import ducktype                                                                     
 12 @ducktype(A)                                                                                    
 13 class B:                                                                                        
 14     def f(self) -> int:                                                                         
 15         return 1                                                                 
 16                                                                                  
 17 quack(B())                   

さらに鋭い方は気付いたかもしれませんが、このコードは Python で実行できません。

(mypy)$ python check_duck_typing.py 
Traceback (most recent call last):
  File "check_duck_typing.py", line 2, in <module>
    def quack(a: A) -> None:
NameError: name 'A' is not defined

Python は前方参照 (forward reference) をサポートしていません。
関数アノテーションで指定したクラス名が解決できなくてエラーになります。mypy は前方参照をサポートしているのでこのままでも実行できますが、それでは実用的に意味がないので文字列リテラルでクラス名を指定することもできます。

  1 # -*- coding: utf-8 -*-                                                                         
  2 def quack(a: 'A') -> None:                                                                      
  3     print(a.f())            

ドキュメントには記載されていませんが、あるスライドに Go 言語風のインターフェースを提供する Protocol というのがあるのを知りました。

  1 # -*- coding: utf-8 -*-                                                          
  2 from abc import abstractmethod                                                   
  3 from typing import Protocol                                                      
  4                                                                                  
  5 class Sized(Protocol):                                                           
  6     @abstractmethod                                                              
  7     def __len__(self) -> int:                                                    
  8         pass                                                                     
  9                                                                                  
 10 def not_empty(x: Sized) -> bool:                                                 
 11     return len(x) > 0                

試しにコードを書いて実行してみたらエラーになりました。

(mypy)$ mypy check_protocols.py 
check_protocols.py, line 3: Module has no attribute 'Protocol'
check_protocols.py, line 5: Name 'Protocol' is not defined
(mypy)$ python check_protocols.py 

Feature proposal: Golang style interfaces · Issue #497 · JukkaL/mypy · GitHub によると、この仕組みはまだ開発中のようです。

class AbstractGenericMeta(ABCMeta):
    ...

class Protocol(metaclass=AbstractGenericMeta):
    """Base class for protocol classes."""

実装をみれば分かるように abc を使った抽象化で実現しています。abc をあまり使ったことがないので私にはまだピンときていませんが、まだまだ議論の余地はありそうです。

共用体 直和型 (Union Types) *1

wikipedia:共用体 と聞くと C 言語を思い浮かべますが 、汎用関数ではよくある処理です。Unionコンストラクタに受け入れられる型を指定します。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Union                                                         
  3                                                                                  
  4 def f(x: Union[int, str]) -> None:                                               
  5     x + 1     # Error: str + int is not valid                                    
  6     if isinstance(x, int):                                                       
  7         # Here type of x is int.                                                 
  8         x + 1      # OK                                                          
  9     else:                                                                        
 10         # Here type of x is str.                                                 
 11         x + 'a'    # OK                                                          
 12                                                                                  
 13 f(1)    # OK                                                                     
 14 f('x')  # OK                                                                     
 15 f(1.1)  # Error                               

型に特化した処理は isinstance で分岐することで mypy の型チェックでエラーになりません。

(mypy)$ mypy check_union_types.py 
check_union_types.py: In function "f":
check_union_types.py, line 5: Unsupported operand types for + ("Union[int, str]" and "int")
check_union_types.py: At top level:
check_union_types.py, line 15: Argument 1 to "f" has incompatible type "float"; expected "Union[int, str]"

Union がなかったらどうしたら良いんだろう?と思ってちょっと試してみました。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Any, cast                                                     
  3                                                                                  
  4 class MyModel:                                                                   
  5     def __init__(self, value: Any) -> None:                                      
  6         self.value = value                                                       
  7                                                                                  
  8 class MyInt(MyModel):                                                            
  9     def __add__(self, other: int) -> int:                                        
 10         return self.value + other                                                
 11                                                                                  
 12 class MyStr(MyModel):                                                            
 13     def __add__(self, other: str) -> str:                                        
 14         return self.value + other                                                
 15                                                                                  
 16 def g(y: MyModel) -> None:                                                       
 17     cast(MyInt, y) + 1                                                           
 18     cast(MyStr, y) + 'a'                                                         
 19     y + 1.1                                                                      
 20                                                                                  
 21 def h(z: Any) -> None:                                                           
 22     cast(int, z) + 1                                                             
 23     z + 'a'
 24     z + 1.1

明示的にキャストするか、Any を使う方法しか思いつかなかったのですが、Any を使うとそもそも静的解析できないのでダメですね。

(mypy)$ mypy check_union_types2.py 
check_union_types2.py: In function "g":
check_union_types2.py, line 19: Unsupported operand types for + ("MyModel" and "float")

余談ですが、汎用関数のような処理は 3.4 から標準に入った singledispatch を使うとすっきり書けます。

  1 # -*- coding: utf-8 -*-                                                          
  2 from functools import singledispatch                                             
  3 from typing import Any                                                           
  4                                                                                  
  5 @singledispatch                                                                  
  6 def f(x: Any) -> Any:                                                           
  7     return x                                                                     
  8                                                                                  
  9 @f.register(int)                                                                 
 10 def _(x: int) -> int:                                                            
 11     return x + 1                                                                 
 12                                                                                  
 13 @f.register(str)                                                                 
 14 def _(x: str) -> str:                                                            
 15     return x + 'a'                                                               
 16                                                                                  
 17 print(f(1))                                                                      
 18 print(f('x'))                                                                    
 19 print(f(1.1))

実行してみます。

(mypy)$ python check_singledispatch.py 
2
xa
1.1

試しに mypy でチェックしてみるとエラーになってしまいました。

(mypy)$ mypy check_singledispatch.py 
check_singledispatch.py, line 2: Module has no attribute 'singledispatch'
check_singledispatch.py, line 5: Name 'singledispatch' is not defined
check_singledispatch.py, line 9: 'overload' decorator expected
check_singledispatch.py, line 13: 'overload' decorator expected

functools のスタブに singledispatch の定義がなかったので以下の定義を追加してスタブディレクトリを環境変数にセットしたら2行目と5行目のエラーは消えました。

from typing import Any

def singledispatch(func: Any) -> Any: pass
(mypy)$ vi stub/functools.py 
(mypy)$ export MYPYPATH=./stub/
(mypy)$ mypy check_singledispatch.py
check_singledispatch.py, line 9: 'overload' decorator expected
check_singledispatch.py, line 13: 'overload' decorator expected

型システムの拡張

かなり満足してきましたが、もうちょっと続きがあります。

今夏にメーリングリストで Guido が 3.5 に取り込もうと提案したのは mypy の型アノテーションの仕組みのみだったように思いますが、mypy には関数オーバーロードジェネリクスといった Python の型システムを拡張する機能も提供しています。ただ、先日投稿された PEP のドラフトには総称型にも言及しているため、さらに突っ込んだ仕組みになるのかもしれません。

関数オーバーロード

オーバーロードって日本語に訳すと wikipedia:多重定義 になるのですね。私の周りではオーバーロードと呼ぶ方が一般的です。

組み込み関数の absオーバーロードしてみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import overload                                                      
  3                                                                                  
  4 @overload                                                                        
  5 def abs(n: int) -> int:                                                          
  6     print('called int version')                                                  
  7     from builtins import abs                                                     
  8     return abs(n)                                                                
  9                                                                                  
 10 @overload                                                                        
 11 def abs(n: float) -> float:                                                      
 12     print('called float version')                                                
 13     return n if n >= 0.0 else -n                                                 
 14                                                                                  
 15 @overload                                                                        
 16 def abs(s: str) -> str:                                                          
 17     print('called str version')                                                  
 18     return s[1:] if s[0] == '-' else s                                           
 19                                                                                  
 20 print(abs(-2))    # int                                                          
 21 print(abs(-1.5))  # float                                                        
 22 print(abs('-3'))  # str

実行してみます。

(mypy)$ mypy check_function_overloading.py 

(mypy)$ python check_function_overloading.py 
called int version
2
called float version
1.5
called str version
3

オリジナルの組み込み関数を呼び出すには builtins からインポートする必要があります。これは関数を探すときにローカルの名前空間から見つけてしまうのと同様です。

>>> type(abs)
<class 'builtin_function_or_method'>
>>> def abs(): pass
... 
>>> type(abs)
<class 'function'>

前節では singledispatchオーバーロードを実現しましたが、mypy の overload は、もうちょと汎用的な multiple dispatch だとドキュメントにあります。

This enables also a form of multiple dispatch.

Function overloading

実際に試してみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import overload                                                      
  3                                                                                  
  4 @overload                                                                        
  5 def func(n: int, s: str) -> str:                                                 
  6     return 'int, str -> str'                                                     
  7                                                                                  
  8 @overload                                                                        
  9 def func(s: str, n: int) -> str:                                                 
 10     print('called str, int -> str')                                              
 11     return func(n, s)                                                            
 12                                                                                  
 13 print(func(1, 's'))                                                              
 14 print(func('s', 1))                  
(mypy)$ python check_multiple_dipatch.py 
int, str -> str
called str, int -> str
int, str -> str

Python の標準ライブラリにはその名の表す通り singledispatch しか導入されなかったわけですが、multiple dispatch の議論もまた活発になるかもしれません。

ジェネリクス

Python のような言語で必要かという疑問はありますが、インターフェイスを明示したいときなどで役に立つのかもしれません。 当初、この例だけをあまりピンと来なかったんですが、コレクションを扱う API を静的解析しようと思ったらジェネリクスがないと不便というのが Javaジェネリクス導入の経緯からも分かることでした (´・ω・`)

ジェネリッククラス (generic class)
  1 # -*- coding: utf-8 -*-
  2 from typing import Generic, List, typevar                                        
  3                                                                                  
  4 T = typevar('T')                                                                 
  5                                                                                  
  6 class Stack(Generic[T]):                                                         
  7     def __init__(self) -> None:                                                  
  8         self.items = List[T]()  # Create an empty list with items of type T      
  9                                                                                  
 10     def push(self, item: T) -> None:                                             
 11         self.items.append(item)                                                  
 12                                                                                  
 13     def pop(self) -> T:                                                          
 14         return self.items.pop()                                                  
 15                                                                                  
 16     def empty(self) -> bool:                                                     
 17         return not self.items                                                    
 18                                                                                  
 19 stack_int = Stack[int]()                                                         
 20 stack_int.push(1)                                                                
 21 stack_int.push(2)                                                                
 22 stack_int.pop()                                                                  
 23 stack_int.push(3)                                                                
 24 stack_int.push('x')                                                              
 25 print(stack_int.items)                                                           
 26                                                                                  
 27 stack_str = Stack[str]()                                                         
 28 stack_str.push(1)                                                                
 29 stack_str.push('x')                                                              
 30 stack_str.push('y')                                                              
 31 print(stack_str.items)

型パラメーターを使うことで型を明示できた、やったー!と思うか、もともと動的型付けなのでこんなことしなくてもそのままコードが書けるのに、、、と思うか。

(mypy)$ mypy check_generic_classes.py 
check_generics.py, line 24: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int"
check_generics.py, line 28: Argument 1 to "push" of "Stack" has incompatible type "int"; expected "str"

実行結果。

(mypy)$ python check_generic_classes.py 
[1, 3, 'x']
[1, 'x', 'y']
ジェネリック関数 (generic function)

同様に関数にも応用できます。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Sequence, typevar                                             
  3                                                                                  
  4 T = typevar('T')                                                                 
  5                                                                                  
  6 def first(seq: Sequence[T]) -> T:                                                
  7     return seq[0]                                                                
  8                                                                                  
  9 print(first([1, 2, 3]))                                                          
 10 print(first('abc'))                                                              
型変数と制限

型変数の応用例です。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import typevar                                                       
  3                                                                                  
  4 AnyStr = typevar('AnyStr', values=(str, bytes))                                  
  5                                                                                  
  6 def concat(x: AnyStr, y: AnyStr) -> AnyStr:                                      
  7     return x + y                                                                 
  8                                                                                  
  9 concat('a', 'b')    # Okay                                                       
 10 concat(b'a', b'b')  # Okay                                                       
 11 concat('a', b'b')   # Error!                                                     
 12 concat(1, 2)        # Error!                   

型チェックをすると11行目と12行目がエラーになります。11行目は str と bytes を連結することはできないからです。

(mypy)$ mypy check_generic_type_variables1.py 
check_generic_type_variables1.py, line 11: Type argument 1 of "concat" has incompatible value "object"
check_generic_type_variables1.py, line 12: Type argument 1 of "concat" has incompatible value "int"

これは一見 Union で代替できそうにみえますが、違うものだとドキュメントにあります。やってみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Union                                                         
  3                                                                                  
  4 def concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]:     
  5     return x + y  # Error: can't concatenate str and bytes                       
  6                                                                                  
  7 concat('a', 'b')    # Okay                                                       
  8 concat(b'a', b'b')  # Okay                                                       
  9 concat('a', b'b')   # mypy passes this line, but error at runtime                
 10 concat(1, 2)        # Error! 

5行目で Union だとサポートされてないといったエラーが出ますが、9行目のコードはパスしてしまいますね。

(mypy)$ mypy  check_generic_type_variables2.py 
check_generic_type_variables2.py: In function "concat":
check_generic_type_variables2.py, line 5: Unsupported operand types for + (likely involving Union)
check_generic_type_variables2.py: At top level:
check_generic_type_variables2.py, line 10: Argument 1 to "concat" has incompatible type "int"; expected "Union[str, bytes]"
check_generic_type_variables2.py, line 10: Argument 2 to "concat" has incompatible type "int"; expected "Union[str, bytes]"

このコードの意図することは、以下のように関数オーバーロードで定義するのと等価ですが、

@overload
def concat(x: str, y: str) -> str:
    return x + y

@overload
def concat(x: bytes, y: bytes) -> bytes:
    return x + y

ジェネリクスの型変数を使うとより短く書けるという利点があります。
なるほど、これはしっくりきました。

まとめ

全てではないですが、mypy の用途や機能のほとんどは俯瞰できたのではないかと思います。おもしろそうだと思ったら是非プロジェクトのサイトも参照してください。

いまの時点では、型アノテーションを指定しても実行時の最適化 (高速化) にはなりません。

Mypy and PyPy are orthogonal. Mypy does static type checking, i.e. it is basically a linter, but static typing has no runtime effect, whereas the PyPy is an Python implementation. You can use PyPy to run mypy programs.

How is mypy different from PyPy?

意訳すると、

mypy と PyPy は直交する。mypy は、基本的には linter のような静的型付けチェックを行うが、実行時には何の影響も及ぼさない。対して、PyPy は Python 実装の1つであり、mypy のプログラムを実行するのに使う。

PyPy で良いことがあるんじゃないかと妄想しますが、PyPy のコア開発者である Alex Gaynor は、型アノテーションが PyPy にとって価値がないと断言しています。彼は型アノテーションの導入よりも、Python の型システムを改善しようと提案していますが。

PS: You're right. None of this would provide *any* value for PyPy.

[Python-ideas] Proposal: Use mypy syntax for function annotations

静的型付けプログラミングっぽいことを Python でやることの是非は分かりません。Python じゃなくて、最初から静的型付き言語を使えば良いじゃないかというのも正しいでしょう。とはいえ、関数アノテーションが mypy を導いたように、型アノテーションが次のなにか (漸進的型付け?) を導く可能性もあります。何よりも動的型付き言語で実行前にエラーチェックできる範囲が広がることは、Python というプログラミング言語が好きな私にとってはとても嬉しいニュースでした。

2014-12-26 追記

実際に小さいコードで試してみました。
python3 - mypy で適当なスニペットを実際に静的型付けしてみた - Qiita

*1:共用体は C 言語で普及した訳語で型システムでは直和型という訳語の方が一般的な表現のようです

Python3 プログラミング勉強会の補足

先日、Python3 プログラミング勉強会 を開催しました。

以下に資料の中で引用したサンプルコードを置いておきました。

ちなみにこういったサンプルコードをアップする前に pep8 と flakes と doctest のテストを実行しておけば typo してツッコミを受けることもありません。例えば pytest 使えば、以下のように簡単にできます。

$ py.test --pep8 --flakes --doctest-module --clear . 
============================= test session starts ==============================
platform linux -- Python 3.4.0 -- py-1.4.20 -- pytest-2.5.2
plugins: flakes, pep8, cache
collected 39 items 

algorithm_set1.py ....
generator_sample1.py ...
generator_sample2.py ......
generator_sample3.py .......
lru_cache_sample1.py ....
singledispatch_sample1.py ....
super_sample1.py ....
test.txt .
with_sample1.py ......
========================== 39 passed in 0.15 seconds ===========================

ページ14: バージョニングのお話

勉強会ではスライドを飛ばしたけど、バージョニングのスキームがすごいことになってます。

[N:]N(.N)*[{a|b|c|rc}N][.postN][.devN]

RPM 以外で Epoch をバージョニングのスキームに組み込んでいるのを私は初めてみました。PEP440 の提案者が Nick Coghlan (@) で、プロフィールをみると Red Hat で働いている開発者だったので元ネタは RPM なのかもしれません。あと、Python の3番目のバージョンをマイクロと呼ぶのも知りませんでした。

ページ16: setuptools の原罪と贖罪

このスライド1枚作るのに何時間もかけて情が移ってしまったために、このスライドを力説し過ぎて時間を浪費してしまいました。この資料の中で一番価値のあるスライドだと思っているいるので、何かに困ったときに見返してください。

ページ17: パッケージングと pbr

あー、pbr の話ももっとしたかったです。これだけで勉強会しても良いぐらいです。OpenStack プロジェクトで何が起きているか、みたいなのは今後の Python の未来に大きな影響を与えるような気がします。

簡単に紹介すると、setup.py にパッケージ情報を書かずに setup.cfg に Metadata2.0 のフォーマットでパッケージ情報を記述できます。

$ git clone https://github.com/openstack-dev/pbr
$ cd pbr/
$ vi setup.py
import setuptools
from pbr import util

setuptools.setup(
    **util.cfg_to_args())
$ vi setup.cfg 
[metadata]
name = pbr
author = OpenStack
author-email = openstack-dev@lists.openstack.org
summary = Python Build Reasonableness
...

git の履歴から AUTHORS や ChangeLog も自動生成してくれます。git のコミットログから ChangeLog を作ってくれるのは結構便利かもしれませんね。コミットログの最初の行を ChangeLog のサマリとして扱ってるようにみえます。マージのコミットログは無視してたりして賢いです。

$ workon pbr3  # pbr や関連ツールをインストールしておいた仮想環境を使う
(pbr3)$ python setup.py sdist
(pbr3)$ head AUTHORS 
Alex Gaynor <alex.gaynor@gmail.com>
Andrew Bogott <abogott@wikimedia.org>
Angus Salkeld <asalkeld@redhat.com>
Anthony Young <sleepsonthefloor@gmail.com>
Attila Fazekas <afazekas@redhat.com>
Ben Nemec <bnemec@redhat.com>
Bhuvan Arumugam <bhuvan@apache.org>
Brian Waldon <bcwaldon@gmail.com>
Chang Bo Guo <guochbo@cn.ibm.com>
ChangBo Guo(gcb) <eric.guo@easystack.cn>
...

(pbr3)$ cat ChangeLog 
CHANGES                                                                        
=======                                                                        
                                                                               
* Allow examining parsing exceptions                                           
* Update integration script for Apache 2.4                                     
* Restore Monkeypatched Distribution Instance                                  
* Register testr as a distutil entry point                                     
* Check for git before querying it for a version                               
* Allow _run_cmd to run commands in any directory                              
* Make setUp fail if sdist fails                                               
* Permit pre-release versions with git metadata                                
* Un-nest some sections of code                                                
                                                                               
0.8.2                                                                          
-----                                                                          
                                                                               
* Remove --use-mailmap as it's not needed                                      
* Fix typos in docs                                                            
                                                                               
0.8.1                                                                          
-----                                                                          
                                                                               
* pbr/testr_command.py: Add logging                                            
* Documentation updates                                                        
* Fixed a typo in the documentation                                            
* Make source configurable when using --coverage                               
* README.rst: tweaks                                                           
* Format autoindex.rst file properly                                           
* make pbr use hacking directly   
...

(pbr3)$ git log
commit e1b98f578a0f94a791210dd48530e2fed43fbe61
Merge: fa17f42 6541911
Author: Jenkins <jenkins@review.openstack.org>
Date:   Mon Jun 30 17:34:07 2014 +0000

    Merge "Check for git before querying it for a version"

commit fa17f42d7d195dab77c042ac4881ac63bfb638f3
Author: Robert Collins <rbtcollins@hp.com>
Date:   Mon Mar 17 09:46:58 2014 +1300

    Allow examining parsing exceptions.
    
    Having to put breakpoints in pbr to diagnose issues is bad for dealing
    with reports from users.
    
    Change-Id: Ifecf4c4e4bb5955e0e5feb4bf5b5b85150b08ebe

commit d815366577a7dbb7a8dab66ab105e2803af4c007
Merge: ec1009c bdb0191
Author: Jenkins <jenkins@review.openstack.org>
Date:   Fri Jun 27 02:29:12 2014 +0000

    Merge "Un-nest some sections of code"

commit ec1009cf197ed555b29b6f226adb300914ea5bd0
Author: Sean Dague <sean@dague.net>
Date:   Thu Jun 26 08:22:47 2014 -0400

    Update integration script for Apache 2.4
    
    Apache 2.4 requires site configuration files to have a ".conf"
    extension, and Apache 2.2 does not want the extension. Add logic to
    figure out the right name for the file so we can run the tests against
    both versions of Apache.
    
    Fixes bug: #1334326
...

テストランナーに tox と testr を使います。setuptools の拡張コマンドに testr オプションが追加されています。デフォルトでテストを並列実行 (CPU の数かな?) してくれるようです。

(pbr3)$ python setup.py testr
running testr
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ . --list 
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpe9wwqlo3
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpbwa1_yy8
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpqv7w0s4v
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpqdbg34vv
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpkd169eki
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpch2tkra0
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmp6h4ef3mr
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmp6407izna
Ran 57 tests in 2.529s (-0.726s)
PASSED (id=9, skips=8)

(pbr3)$ python setup.py testr --testr-args --concurrency=1
running testr
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  
Ran 57 tests in 9.140s (-0.065s)
PASSED (id=7, skips=8)

ページ21: Pythonic なコード

エキスパート Python プログラミングから引用しました。

Pythonic とは、小さなコードパターンに最も効率的なイディオムを使ったコーディングや構文

最近、他人の Python コードを読んでいて「何かおかしい」と違和感を感じるときは、この Pythonic の概念を大事にしてコードを書いているかどうかの違いによるもののような気がしてきました。OSSフレームワークやライブラリを使うときにも、その開発者の意図に共感できると親しみがもてたりするものです。

Pythonista というと、一貫性にこだわる怖い人たちなんでしょう?のイメージがあります。もちろんそういった場面にもよく遭遇しますが、

Although practicality beats purity. ( とはいえ、純粋さよりも実用的であること )

  • PEP8 の一節

A Foolish Consistency is the Hobgoblin of Little Minds

とか言っていて、一貫性にこだわり過ぎるのもよくないという戒めもあります。こだわりは強くても極論はよくないといった感じですかね。

ページ23: functools.singledispatch

汎用関数が気持ちよく書ける、デコレーターの応用例の紹介をしました。
勉強会でも singledispatch をコードリーディングしたのですが、ソースもシンプルなのでデコレーターの実用例としておもしろいと思います。

ページ33: Python 2.4 との互換性を気にする

これを気にしなくなって Python3 に感激したのも事実の1つです。

標準モジュールがなかったり、機能のバージョン毎の差異を吸収するように実装したものがいくつかありました (ここにサンプルコードを置きました) 。2.4 対応を考慮しながらコードを書くと、常にこういったことに悩まされながらコードを書いているという例です。あくまで自分の用途にあわせた実装なのでサンプルコードは参考程度にみてください。

  • json の読み込み
  • itemgetter の複数引数の対応
  • datetime.strptime の互換関数

ページ34: 集合型 set の応用例

これもスライドを飛ばしたけど、集合型をこんな用途にも使えるんだなぁと感心したスニペットです。アルゴリズムを勉強したくなる気を起こさせるコードでした。

def has_invalid_fields_loop(fields):                                             
    """                                                                          
    >>> has_invalid_fields_loop(['foo', 'bar', 'foo', 'test'])                      
    True                                                                            
    >>> has_invalid_fields_loop(['foo', 'bar'])                                     
    False                                                                           
    """                                                                             
    for field in fields:                                                            
        if field not in ['foo', 'bar']:                                             
            return True                                                             
    return False                                                                    
                                                                                    
                                                                                    
def has_invalid_fields_set(fields):                                                 
    """                                                                             
    >>> has_invalid_fields_set(['foo', 'bar', 'foo', 'test'])                       
    True                                                                            
    >>> has_invalid_fields_set(['foo', 'bar'])                                      
    False                                                                           
    """                                                                             
    return bool(set(fields) - set(['foo', 'bar']))

ページ35: super はスーパーな話

簡単な分かりやすいサンプルコードを提示したつもりだったのに、絶賛大ブーイングを受けたサンプルコードがこれでした。こんな複雑なことをする意図が分からない的な。

メソッド解決順序(MRO) がとても分かりやすいけれど、全てのクラスが object を継承するようになったため、どんな多重継承を行っても必ずダイヤモンド継承になる。多重継承が本当に必要かどうかの議論はもちろんあって良いですが、mixin 的に機能を扱いたいときとか、普通にその MRO や移譲先を意識して設計するでしょうし、サブクラスで拡張することを意識してコア機能作ったりするんじゃないかなぁとも思います。フレームワークプラグインのような、それぞれの開発者が異なるケースでもたくさん応用例がありそうに思いますが、ちゃんと調べないと分からないぐらいにはややこしいです。

後半で時間がなかったからちゃんと背景を説明をしなかったせいでもあるけれど、もっと私も勉強しないといけないです。

ページ42: The Hacker's Guide to Python

Python WeeklyThe Hacker's Guide to Python が紹介されていて、機会があったら読もうと思っていました。ちょうど勉強会するからと読み始めたらおもしろくて、この本を読んだために勉強会の内容もかなり変わってしまったぐらい影響を受けました。本当は AST やパフォーマンスの章も紹介したかったけれど、準備が全然間に合わなくて断念しました。

まとめ

当初は社内の Python ツールリファクタリング的な話をしながら Python3 楽しいよという話をしようと構想していました。いろいろ調べたり、The Hacker's Guide to Python を読んだりしているうちにどんどん変わっちゃってこんな感じになってしまいました。せっかく資料をまとめたので何かの役に立ったら嬉しいです。

Ubuntu 14.04 で vim パッケージをリビルドする

Ubuntu 14.04 で vim をインストールすると python3 インターフェースが無効になっています。python3 インターフェースを有効にするために deb パッケージをリビルドしてみます。vim拡張機能をチェックするには以下のようにバージョンを表示します。

$ vim --version  # vim.gtk を使ってます
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Jan  2 2014 19:39:59)
適用済パッチ: 1-52
Modified by pkg-vim-maintainers@lists.alioth.debian.org
Compiled by buildd@
Huge 版 with GTK2 GUI.  機能の一覧 有効(+)/無効(-)
+acl             +farsi           +mouse_netterm   +syntax
+arabic          +file_in_path    +mouse_sgr       +tag_binary
+autocmd         +find_in_path    -mouse_sysmouse  +tag_old_static
+balloon_eval    +float           +mouse_urxvt     -tag_any_white
+browse          +folding         +mouse_xterm     +tcl
++builtin_terms  -footer          +multi_byte      +terminfo
+byte_offset     +fork()          +multi_lang      +termresponse
+cindent         +gettext         -mzscheme        +textobjects
+clientserver    -hangul_input    +netbeans_intg   +title
+clipboard       +iconv           +path_extra      +toolbar
+cmdline_compl   +insert_expand   +perl            +user_commands
+cmdline_hist    +jumplist        +persistent_undo +vertsplit
+cmdline_info    +keymap          +postscript      +virtualedit
+comments        +langmap         +printer         +visual
+conceal         +libcall         +profile         +visualextra
+cryptv          +linebreak       +python          +viminfo
+cscope          +lispindent      -python3         +vreplace
...

自分で vim のソースをダウンロードしてきてビルドする方法もありますが、なるべくディストリビューションのもの (と同じ設定) を使いたいと思う人もいるでしょう。ここでは deb パッケージをリビルドすることで python3 インターフェースを有効にしてみます。

前準備

まずビルドに必要な依存パッケージをまとめてインストールします。私の環境では以下のパッケージを追加するとビルドできましたが、これはそれぞれの環境によって違うかもしれません。

$ sudo apt-get build-dep vim  # vim のビルドに必要な依存パッケージをインストール
$ sudo apt-get install python3-dev ruby-dev tcl8.5-dev  # vim の拡張機能のビルドに必要な依存パッケージをインストール
$ sudo apt-get install devscripts  # deb パッケージのビルドツールをインストール

次にソースをダウンロードします。

$ mkdir /tmp/vim-build
$ cd /tmp/vim-build/
$ apt-get source vim
$ ls
vim-7.4.052  vim_7.4.052-1ubuntu3.debian.tar.gz  vim_7.4.052-1ubuntu3.dsc  vim_7.4.052.orig.tar.gz
$ cd vim-7.4.052
  • vim の configure オプションを調べる
$ ./configure --help
ビルド

バージョン番号もここで指定したものに置き換えられるようです。ディストリビューション名やメールアドレスなど、適切な内容を changelog に書かないとビルド時にエラーになります。

$ debchange -i
vim (2:7.4.052-1ubuntu4) trusty; urgency=medium
                                                                               
  * enabled python3 interface.
                                                                               
 -- t2y <email>  Sat, 26 Apr 2014 18:43:45 +0900
  • rules ファイルの編集

python3 インターフェースを有効にするための configure オプションを追加します。また vim-nox のテスト中にエラーになったのでここではビルド後にテストを実行しないように編集しています。

$ vim debian/rules
--- orig-vim-7.4.052/debian/rules	2014-04-26 15:42:19.264743944 +0900
+++ vim-7.4.052/debian/rules	2014-04-26 16:42:47.818878711 +0900
@@ -27,11 +27,11 @@
 BUILDER := $(shell echo $${DEBEMAIL:-$${EMAIL:-$$(echo $$LOGNAME@$$(cat /etc/mailname 2> /dev/null))}})
 
 MAKETEST := no
-ifeq ($(DEB_HOST_GNU_TYPE),$(DEB_BUILD_GNU_TYPE))
-  ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
-    MAKETEST := yes
-  endif
-endif
+#ifeq ($(DEB_HOST_GNU_TYPE),$(DEB_BUILD_GNU_TYPE))
+#  ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+#    MAKETEST := yes
+#  endif
+#endif
 
 ifneq (,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
     NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
@@ -108,7 +108,7 @@
 ALLINTERPFLAGS+=--disable-mzschemeinterp
 ALLINTERPFLAGS+=--enable-perlinterp
 ALLINTERPFLAGS+=--enable-pythoninterp --with-python-config-dir=$(shell python-config --configdir)
-ALLINTERPFLAGS+=--disable-python3interp
+ALLINTERPFLAGS+=--enable-python3interp --with-python3-config-dir=$(shell python3-config --configdir)
 ALLINTERPFLAGS+=--enable-rubyinterp
 ALLINTERPFLAGS+=--enable-tclinterp
  • ビルド
$ debuild -us -uc
$ ls ../  # 親ディレクトリにパッケージが作成される

インストール

必要なパッケージをインストールします。私は vim-gtk を使っています。

$ cd ../
$ sudo dpkg -i vim_7.4.052-1ubuntu4_amd64.deb \
               vim-common_7.4.052-1ubuntu4_amd64.deb \
               vim-doc_7.4.052-1ubuntu4_all.deb \
               vim-runtime_7.4.052-1ubuntu4_all.deb \
               vim-tiny_7.4.052-1ubuntu4_amd64.deb \
               vim-gui-common_7.4.052-1ubuntu4_all.deb \
               vim-gtk_7.4.052-1ubuntu4_amd64.deb 

$ dpkg -l | grep vim
ii  vim               2:7.4.052-1ubuntu4     amd64     Vi IMproved - enhanced vi editor
ii  vim-common        2:7.4.052-1ubuntu4     amd64     Vi IMproved - Common files
ii  vim-doc           2:7.4.052-1ubuntu4     all       Vi IMproved - HTML documentation
ii  vim-gtk           2:7.4.052-1ubuntu4     amd64     Vi IMproved - enhanced vi editor - with GTK2 GUI
ii  vim-gui-common    2:7.4.052-1ubuntu4     all       Vi IMproved - Common GUI files
ii  vim-runtime       2:7.4.052-1ubuntu4     all       Vi IMproved - Runtime files
ii  vim-tiny          2:7.4.052-1ubuntu4     amd64     Vi IMproved - enhanced vi editor - compact version

オリジナルの vim --version の出力と比べると python3 インターフェースが有効になっているのが分かります。

$ diff -u orig-vim.gtk-version.txt <(vim --version)
--- orig-vim.gtk-version.txt	2014-04-26 15:48:29.234466537 +0900
+++ /dev/fd/63	2014-04-26 17:12:13.006045318 +0900
@@ -1,7 +1,7 @@
-VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Jan  2 2014 19:39:59)
+VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Apr 26 2014 16:45:41)
 適用済パッチ: 1-52
 Modified by pkg-vim-maintainers@lists.alioth.debian.org
-Compiled by buildd@
+Compiled by t2y@...
 Huge 版 with GTK2 GUI.  機能の一覧 有効(+)/無効(-)
 +acl             +farsi           +mouse_netterm   +syntax
 +arabic          +file_in_path    +mouse_sgr       +tag_binary
@@ -18,8 +18,8 @@
 +cmdline_info    +keymap          +postscript      +virtualedit
 +comments        +langmap         +printer         +visual
 +conceal         +libcall         +profile         +visualextra
-+cryptv          +linebreak       +python          +viminfo
-+cscope          +lispindent      -python3         +vreplace
++cryptv          +linebreak       +python/dyn      +viminfo
++cscope          +lispindent      +python3/dyn     +vreplace
 +cursorbind      +listcmds        +quickfix        +wildignore
 +cursorshape     +localmap        +reltime         +wildmenu
 +dialog_con_gui  +lua             +rightleft       +windows

python/dyn, python3/dyn に変わってしまいました。vim のドキュメントによると、Windows だと DLL を探すように書いてあります。Linux だと関係ないのかな?これでも Python 拡張が動いたので気にしないことにします。

$ vim
:help python-dynamic
----
9. Dynamic loading                                      python-dynamic         
                                                                               
On MS-Windows the Python library can be loaded dynamically.  The :version      
output then includes +python/dyn.                                              
                                                                               
This means that Vim will search for the Python DLL file only when needed.      
When you don't use the Python interface you don't need it, thus you can use    
Vim without this DLL file.

考える生き方

極東ブログ の著者の著書を読んだ。

それまでブログの存在は知っていたし、twitter 経由でリンクから記事を読んだことも何度かあった。著者がアルファブロガーの1人だということも知っていた。著者のブログである記事の内容を覚えている。いま見返すと、2年前の記事だった。2年前の記事を何となく読んだだけで未だに記憶に留めているというのは、よくよく考えてみるとすごいことだ。

引用が長くなるので結論だけ紹介するが、この結びが強く印象に残っている。

そういう無名の社会人が困難な状況のなかで「公」を支えてくれるおかげで、私たちは生きていけるわけです。みなさんの、誰かもそうなります。

 だから、いつも思うのです、ありがとう。

4月から新社会人になる皆さんへ: 極東ブログ

本書は何か

たぶん何でもないの只の自伝だと著者なら言うかもしれない。著者は自分の生き方や過去を「からっぽの人生」だったと振り返っているが、本書を読む限り、そんな自虐するようなものでもないように思えた。普通の人の、普通の人生も、端からみると全然普通じゃないというのを端的に表しているのかもしれない。

著者が「からっぽ」だというのは、世の中で理想とされるようなレールに則った人生ではなかったということを指しているのかもしれない。それは社会的に承認されることのない生き方だったのかもしれない。著者の年齢が55歳とあるので、いまよりもずっとそういったレールから外れた生き方に理解がなかったり、厳しかった時代だと思う。

著者のブログのファンならば興味深く読めると思う。私は逆だけど、本書を読んで著者の人となりを知って rss reader に追加した方だ。ブログでもそうだけど、読みやすく、ユーモアを交え、押し付けがましくない著者の文体に好感がもてる。著者は否定しても、アルファブロガーというのはやはりちょっと普通じゃないと思うので、普通の人が普通の生きるための参考になるかは正直分からない。でも、誰彼もその人の人生であって、それが普通か普通でないかは、さして意味はないというのを考えさせられる気もする。

テーマが多い

著者の半生を綴ったものだからテーマが多い。仕事、家族、恋愛、難病、学問、社会、文化、人生、、、色んなことが書いてある。その分、著者の主張のようなものがあるわけではない。ブログの延長と言えばそうかもしれないが、本書は誰かのための、何かのためのものという目的では書かれていない。読者が読んで何かを考えるきっかけになるようなものだと思う。

私の中では、最もおもしろかったのは、第5章の「勉強して考えたこと」でした。著者の勉強に対する考え方や向き合ってきた姿勢が読み取れて興味深かった。

勉強する意味

著者の勉強する意味とはこうらしい。

私は、楽しいからだと思う。もちろん、私がそういうタイプの人だからというのはある。
(中略)
知るということ、それ自体が楽しい。子どものころは、ほとんどの人がそうだった。それをいくつになっても維持していくようにするのが勉強ではないかと思う。

著者と自分の共通する点が1つある。それは職を転々と変えてきたところだ。一般的に職を変えることはあまり良いことではないだろう。ただ、考えるという点において環境を変えるというのは大きな影響力をもつように思う。

考える習慣を持たない人に共通する問題点として 「無知の知」 ならぬ 「無知の無知」 があります。 自分が分かってないことを分かってない、 自分の浅慮では到底及ばない領域があることが全く想像できない、 いわば 「井の中の蛙」 状態ですね。

仙石浩明の日記: 「無知の無知」への 3ステップ

また別のブログの引用だけど、またしても新社会人向けに書かれた記事だw、なんだ?そんなことも分かってないのか自分、、、という気分だ。

長く同じ環境にいると人間は慣れる。慣れると、考えずに行動できるようになる。それ自体が悪いわけではないけど、人間は楽をする生きものだから無意識にものごとを深く考えないようになってしまうことがあるように思う。そのときにこの「無知の無知」が起こる気がする。私の場合、転職を繰り返すうちに、前の職場で通用した知識やスキルが次の職場では全く通用しないことが起こる。そんなときに強く実感する。自分は何も分かってなかったんだなぁと。

私にとっての、勉強する意味は2つある。

一つは、勉強してないと、この先生き残れないんじゃないかという不安がある。周りの人たちも当たり前のように勉強しているのを見てきたのでそこに疑問はないし、勉強せずに活躍している人もやはり見たことがない。もう一つは、分からないと悔しい。気持ち悪い。恥ずかしい。そういった負の感情へのコンプレックスがある。

著者の知りたいという動機とは純粋度が違う。違う分、業務の勉強なんかには向くけど、自然科学とか、興味がないものの勉強には食指が動かない。

国際性の中のエピソード

著者の大学時代の、留学生とのやり取りのエピソードでおもしろいものがあった。

「NO」と言えるのは、人間が対等なときだ。人間が人間であるなら、違う考えをもって表明するときは、「NO」という。

気さくな友だちには NO と言えても、そうじゃない知り合いとか、同僚とか、そういう人たちに NO と言いにくかったりするのは、そういった背景もあるんだなぁと省みてしまった。ここで言う対等というのは立場的なものじゃなくて人間関係なようにも思う。

リベラル・アーツという概念

この節がすごくおもしろかった。この節を読むだけで純粋に勉強する楽しみというのはこういうことなんだなと分かった気になる。

メカニカル・アーツの対立語としての wikipedia:リベラル・アーツ について述べている。この wikipedia の説明もおそらくは正しいのだろうけど、本書と比べて、その時代の背景や文化的な側面があまり書かれていない。辞書を引けばその定義が書いてある。そういうものだけど、何だかしっくりこない。

本書では、まず「リベラル」とは自由市民を指していて、自由市民とは、奴隷とは、枝芸とは、そういったあれを知るためにはこれを知る必要があって、背景が分かった上で最終的にはこうなんですと展開している。それはほんの上辺でしかないけど、体系的に学ぶというのはそういうことかとも思う。そして、それがおもしろいから勉強するんだなということも分かる。決して wikipedia を作るために勉強するわけではないんだ。

リベラル・アーツが何ものかが分かったところでその必要性を説く。きっと著者の講義を受けられるとしたらこんな感じで勉強する楽しさを教えてくれるように思う。

また余談だけど、art という単語から想像する日本語は「芸術」だろう。実際、その訳語が第一義だから正しいのだけど、辞書を引くと art という単語には「技術」や「技能」といった訳語も出てくる。ここで言う、リベラル・アーツの art というのは、徒弟訓練で学ぶ技能の体系のことを指すらしい。

"The Art of Computer Programming", "The Art of UNIX Programming" など、Art of と銘を打つコンピューター関係の書籍はちらほらある。それらのイメージを単純に私は「芸術」と捉えていたのだけど、なんだ技術体系の書籍だという見方もあるのかと再発見した次第だ。

終わりに

そして、やはり結びの言葉も良い。

こう言ってもいいのかもしれない。人生の敗北者であっても、貧しい生活でも、学ぶことで人生は豊かになる。

希望はある。

考える生き方

考える生き方

パーフェクトPython

Python3 に特化した専門書という位置付けですが、(Python3 に関した) 言語仕様やその変更、ライブラリの詳細の違いなどを除けば、Python2 でも活用できる知識が大半です。まだ Python2 しか使っていないという方でも十分に役立つ内容だと思います。

ただ、本書を読み進める上で1つだけ忘れてはいけないことがあります。Python 全般について丁寧に解説されていますが

著者名が Python サポーターズとなっていますが、ついったーなんかでは Python モヒカンズなんて言われています。

パーフェクト Python の執筆に参加しました — プログラマのネタ帳 二冊目

と何だかこわい人たちが書いた本だということです。まずはそのことを念頭におくことで本書を楽しんで読み進められる心の準備が整った、言い換えると、覚悟はできたと言えます。そうすれば、途中でこわくなってきても勇気をもって立ち向かえますからね。

気になったところや興味深かったところを付箋付けしていったらこんなことになってしまいました。こわいことがたくさんです。

目次は以下の通りです。

1章 Pythonの概要

  • Part2 言語仕様

2章 Pythonの基本
3章 型とリテラル
4章 制御構文
5章 関数
6章 クラス
7章 モジュールとパッケージ
8章 拡張モジュールと組み込み
9章 標準ライブラリ

  • Part3 実践的な開発

10章 コマンドラインユーティリティ
11章 チャットアプリケーション
12章 アプリケーション/ライブラリの配布
13章 テスト
14章 Webプログラミング

  • Part4 適用範囲

15章 学術/分析系ライブラリ
16章 マルチメディア
17章 ネットワーク
18章 データストア
19章 運用/監視

Appendix
A 環境構築
B 標準ライブラリ

章ごとにこわくなったところをみていきます。

追記:
内輪的なものが気になるという意見も頂きました。
悪い意図はなかったので気になる方は以下のように置き換えて読んでください。

こわい = (個人的に) 興味がある、気になる、分からなかった、おもしろかった、不思議に思った

1章 Pythonの概要

Python の禅

Python の禅を全て紹介していた書籍は、私が読んだ中ではなかったように思います。こういった思想的なものは、最初のうちは言葉通りでしか理解できませんが、Python でプログラムを書くのに慣れていったり、開発コミュニティでやり取りしていくうちに、議論の根幹の部分の、共感できる要素になっていったりする気もします。

最初に思想的なところから啓蒙してくる辺り、Python サポーターズに洗脳されるんじゃないかと警戒したくなります。まだ初めの方なのでこわがらずに Python サポーターズの価値観に共感して読み進めましょう。

  • Beautiful is better than ugly.

最後の方に PerlRuby正規表現Python との違いをさらっと紹介しています。Python は言語の機能として正規表現をサポートせず、re という標準モジュールにより提供していることの背景を期待して読みましたが、、、書かれていませんでした。

なまじ説明がないのは「分かれよ」というメッセージです。Python正規表現リテラルをサポートしないのは、記号の多用を避けることを意図したものだったような気がしますが、情報ソースがどこだったのか見つけられませんでした。私の解釈があっているのか、ちょっとこわいです。

  • Flat is better than nested.

Java のパッケージの名前空間は長くて冗長だというものに対する否定やユーモアからの禅です。厳密に言うと、Java のパッケージは、ファイルシステムの階層構造と一致させる必要はありませんが、慣習的に (分かりやすさのために) そうなっているようです。Python のパッケージは __init__.py を置く決まりになっているのでファイルシステムと一致させる必要があります。データ構造やプログラミングの階層構造のことだと思わせておいて Java を dis ってるのがこわいです。

column: 名前の由来

本書では、spam や egg という変数名を使ったサンプルコードがたくさん紹介されていて、モンティ・パイソンから引用したものだとコラムにあります。

プログラムの挙動を説明するときなど、短いスニペットに使うときもありますが、意味のない名前を付けるのはなるべく避ける方が良いです。というのは、プログラミングの重要な要素の1つに「名前を付ける」ということがあります。ここで言うのは以下のような意味です。

  • 適切な名前を付ける
  • 分かりやすい名前を付ける
  • 簡潔な名前を付ける

この名前付けというのは、簡単なようでプログラムの規模が大きくになるにつれ難しくなります。変数名に限らず、小さい関数に分割しても関数名で悩み、クラス名で悩み、モジュール名で悩みます。その処理そのもの、責務、意図といった多くの概念を簡潔に表そうと考えるほど、名前を付けるのは難しいものです。

spam や egg というのは、ただの名前の過ぎないので、プログラマーにとって重要な「名前を付ける」という素養を軽視するのは、あまり良くない習慣だと言えます。こんなことを書いて大丈夫か、自分がこわいです。

IPython のオートコンプリート機能

IPython の機能の1つとしてコード補完が紹介されています。

私も普段は IPython を使っていますが、サーバーでのデバッグなど、IPython がインストールされていない環境でコード補完を行いたいときもあるでしょう。readline モジュールがインストールされている必要がある点は同じですが、IPython をインストールしなくても Python の標準ライブラリでコード補完できます。

>>> import readline, rlcompleter
>>> readline.parse_and_bind('tab:complete')

覚えておくと、ちょっとしたときに便利です。

たぶん Python サポーターズは、コード補完なんて軟弱な機能を使っていません。こわいです。

3章 型とリテラル

ucs2 と ucs4

PyCon JP 2012 で Dan Kogai 氏が指摘されていたのが印象に残っています。

Python 3.2 まではビルドオプションで ucs2 と ucs4 のどちらかを指定していました。この違いにより、16bit で表せるコードポイントを超えた部分に定義された文字列長が違ってしまうという問題がありました。Python 3.3 (PEP 393) からその問題が解消され、正しい文字列長が返されるようです。Python後方互換性を重視するため、マイナーバージョンアップにより以前書いたコードが動かなくなることはありませんが、あるバージョンから内部の仕組みが変わることはあります。

多くの人はディストリビューションが提供するバイナリを使うため、どちらのビルドオプションか意識せずに使っているでしょう。いまが過渡期であることの、問題の1つですね。

Python サポーターズは自分でビルドしているそうです。こわいです。

sys.intern

私自身、こういった最適化を行いたい状況に遭遇したことはありませんが、http://d.hatena.ne.jp/atsuoishimoto/20110418/1303094478 で知り、そんな仕組みがあるんだと印象に残っている機能です。

>>> import sys
>>> 'spam'.lower() is 'spam'.lower()
False
>>> sys.intern('spam'.lower()) is sys.intern('spam'.lower())
True

これはこわくありません。

タプルの構文

要素が1つの場合には tuple である事が解らなくならないように、(1,) のように末尾にカンマを記述します。

これは少し誤解を招く表現です。というのは、分からなくならないようにこのような記述をするのではなく、tuple の構文はカンマで区切るものであり、カッコは人間が分かりやすいように付けているものだからです。

>>> type((1))
<class 'int'>
>>> type((1,))
<class 'tuple'>
>>> 1,
(1,)

要素1のリストはカンマが必要でないので混同しがちなところかもしれません。

>>> [1]
[1]
>>> type([1])
<class 'list'>

カンマを忘れて tuple のつもりが int だったりしたらこわいです。

追記: 補足もらった、ありがとう

4章 制御構文

column: 内包表記のメリット

シンプルなループは積極的に内包表記を使った方がパフォーマンス面でメリットがあります。

>>> r = [i for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> type(r)
<class 'list'>

私は map の方がコーディング的には好みですが、Python3 からはリストが返されるのではなく map オブジェクトが返されるようになりました。

>>> r = map(lambda x: x, range(10))
>>> type(r)
<class 'map'>
>>> r[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'map' object is not subscriptable

map オブジェクトは、ジェネレーターとしては判別されないみたいですね。

>>> import types
>>> isinstance(r, types.GeneratorType)
False

結果をリストで扱いたいときは組み込み関数の list で変換できますが、そういった用途なら内包表記を使う方が簡潔で良いですね。

>>> r = list(map(lambda x: x, range(10)))
>>> r[1]

従来からのリスト内包表記に加えて、セット内包表記、辞書内包表記も使えるようになりました。

>>> {x for x in [1, 2, 3, 2, 1, 5]}
{1, 2, 3, 5}
>>> {key: value for key, value in zip(['a', 'b'], [1, 2])}
{'b': 2, 'a': 1}

(慣れの問題で) ちょっと違和感も感じますが、コーディング的におもしろいです。こわくはありません。

例外オブジェクトの特殊メソッド

前後に __ (私はアンダーアンダーと呼んでます) で囲まれたメソッドを、Python では特殊メソッドと呼びます。ある人は、特殊メソッドは「Python の黒魔術だ」と表現していましたが、どんな特殊メソッドがあるのか知っておくというのは Python のプログラミングで重要なことです。

例外オブジェクトで3つの特殊メソッドが紹介されていました。

  • __traceback__: 例外発生時のトレースバックが格納される
  • __context__: 例外発生中に例外が発生したとき、元の例外オブジェクトが格納される
  • __cause__: "raise Error from err" としたとき、元の例外オブジェクトが格納される

この手のものは、時間とともにどんどん増えていきそうな雰囲気を感じます。便利なのは便利なのでしょうが、バージョンにより使える特殊メソッドがあるというのは意識しておく必要がありそうです。

Python サポーターズは、特殊メソッド を全て暗記しているそうです。こわいです。

5章 関数

サブジェネレーターへの委譲

Python 3.3 の目玉機能の1つです。

def generator():
    yield from sub_generator()

この処理をちゃんと実装しようとすると、かなり煩雑になるというのが PEP 380 Formal Semantics にあります。PEP 380 は粗訳したまま放ったらかしです、こわいというかごめんなさい。

6章 クラス

super とメソッドの検索順序 (mro)

Python3 から super を呼び出す引数を省略できるようになったのが簡潔で良いですね。

super と mro は、Python で委譲を実装する仕組みの1つです。super のメリットが分かる簡単なサンプルを紹介します (Pythonのsuper()ができること から)

  • 継承元の基底クラスを変更するときにコードを書き換える必要がない
  • サブクラス側でスーパークラスのデフォルトの委譲先を変更できる
>>> class MyDict(dict): pass
... 
>>> MyDict.__mro__
(<class '__main__.MyDict'>, <class 'dict'>, <class 'object'>)
>>> class YourDict(dict): pass
... 
>>> class OurDict(MyDict, YourDict): pass
... 
>>> OurDict.__mro__
(<class '__main__.OurDict'>, <class '__main__.MyDict'>, <class '__main__.YourDict'>, <class 'dict'>, <class 'object'>)

MyDict の mro は、MyDict->dict->object の順番だったのが、YourDict を定義して、OurDict で MyDictとYourDict を多重継承した場合、MyDict の次の mro が YourDict に変更されていることが分かります。例えば、MyDict の __init__ でスーパークラスの __init__ の呼び出しを super で行った場合、MyDict を修正することなく dict.__init__ から YourDict.__init__ へ変更できるというわけです。

>>> class MyDict(dict):
...   def __init__(self):
...     super().__init__()  # mro により、dict か YourDict かを解決してくれる

本書のサンプルにあった、多重継承の「もう少し複雑なケース」はよく分からなかったので読み飛ばしました。初心者に易しくないのがこわいです。

7章 モジュールとパッケージ

相対インポートと名前空間パッケージ

サードパーティのライブラリをインポートしたり、拡張プラグインを開発するときなど、ちょくちょくはまった経験があります。相対インポートと名前空間パッケージについて簡潔に説明されていて分かりやすいです。PEP 420 で Python3.3 からは名前空間パッケージが言語機能として提供されるようになったようです。

従来の pkg_resources によるおまじないは不要になるのですね。

__import__('pkg_resources').declare_namespace(__name__)

Python のパッケージングの仕組みは、一体どのライブラリの機能なのかよく分からなくなるのがこわいです。

9章 標準ライブラリ

組み込み型関数の memoryview

バッファプロトコルをサポートしたデータを扱い、コピーを作る事なく操作できるクラスです。

こんな組み込み型関数があったんだと初めて知りました。

>>> m = memoryview(b'abc')
>>> type(m)
<class 'memoryview'>

Python2.7 でも (バックポートされて?) 使えるみたいですね。

IO 周りのどんな処理や用途に使うと良さそうなのか、実例を見てみたいです。私はこわくて声をかけられませんが、誰か Python サポーターズに聞いてみてほしいです。

map の特殊な使い方

Python2 では map の第1引数に None を渡すと、足りない要素数を None で補う zip のような挙動をしていました (ライブラリリファレンスの map() の説明) 。

>>> map(None, [1,2,3], [4,5], [7,8,9])
[(1, 4, 7), (2, 5, 8), (3, None, 9)]
>>> zip([1,2,3], [4,5], [7,8,9])
[(1, 4, 7), (2, 5, 8)]

Python3 からはこのように扱ってくれないようです。

>>> list(map(None, [1,2,3], [4,5], [7,8,9]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
>>> list(zip([1,2,3], [4,5], [7,8,9]))
[(1, 4, 7), (2, 5, 8)]

変わりに itertools.zip_longest を使うようです。

>>> from itertools import zip_longest
>>> list(zip_longest([1,2,3], [4,5], [7,8,9]))
[(1, 4, 7), (2, 5, 8), (3, None, 9)]

2to3 では変換できなかったので移行のちょっとした tips ですね。

$ 2to3 map-none.py 
--- map-none.py (original)
+++ map-none.py (refactored)
@@ -1 +1 @@
-map(None, [1,2,3], [4,5], [7,8,9])
+list(map(None, [1,2,3], [4,5], [7,8,9]))

Python サポーターズは 2to3 を使わずに自分でコードを書き直すようです。こわいです。

pdb コマンド

インタラクティブデバッガも標準で付属していて必要十分なのが嬉しいですね。ツールが揃っていると、デバッグも楽しいものです。

  • pretty print

複雑なオブジェクトを調べるには from pprint import pprint してたのですが、pp でできることを知りました (> <)

(Pdb) list
  1     d = {'a': 1, 'b': 2, 'l': range(10), 't': tuple(range(10)), 'd': dict(x=9)}
  2  -> import pdb; pdb.set_trace()
[EOF]
(Pdb) p d
{'t': (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 'l': range(0, 10), 'a': 1, 'd': {'x': 9}, 'b': 2}
(Pdb) pp d
{'a': 1,
 'b': 2,
 'd': {'x': 9},
 'l': range(0, 10),
 't': (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)}
  • 条件付きブレイク

なぜか私の中では pdb で 条件付きブレイクはできないと思い込んでいました。これも Python2 の頃からできたんですね (- -#)

> /Users/t2y/work/book/review/perfect-python/pdb/pdb_sample2.py(3)<module>()
-> def f(x):
(Pdb) longlist
  1     import pdb; pdb.set_trace()
  2     
  3  -> def f(x):
  4         # do something
  5         return x
  6     
  7     for i in range(5):
  8         print(f(i))
(Pdb) break 5, x==3
Breakpoint 1 at /Users/t2y/work/book/review/perfect-python/pdb/pdb_sample2.py:5
(Pdb) c
0
1
2
> /Users/t2y/work/book/review/perfect-python/pdb/pdb_sample2.py(5)f()
-> return x
(Pdb) x
3
(Pdb) c
3
4
$ 

Python サポーターズの書くプログラムにはバグがないのでデバッガの使う機会はないんだと推測します。こわいです。

column: IDE

PythonIDE でメジャーなものを紹介していますが、あれ!?と思ったのが漏れていたので紹介します。

きっと Python サポーターズは IDE を使っていません。こわいです。

10章 コマンドラインユーティリティ

10-3-5 データを表示する

集計したデータを表示するところで、さらっと operator モジュールが使われていました。

def print_results(results):                                                         
    for key, value in sorted(results.items(), operator.itemgetter(1)):              
        print(key, value)    

operator モジュールの用途の1つとして、要素のゲッターとして使うとコードが簡潔に記述できるので、私は好んでよく使います。

>>> import operator
>>> d = {"x": 1, "y": 2}
>>> operator.itemgetter("x", "y")(d)
(1, 2)

>>> f = open('test.txt', mode='w', encoding='utf-8')
>>> operator.attrgetter('mode', 'encoding')(f)
('w', 'utf-8')

同処理をする Python の関数や lambda を使うよりパフォーマンス面でもメリットがあると、以前はドキュメントに記述されていた気がします。operator, functools, itertools, contextlib, collections といった標準ライブラリを使うようになってきたら段々こわくなる気がします。

12章 アプリケーション/ライブラリの配布

distribute の注意点

distribute には、プロジェクト内のパッケージを自動で発見する find_packages という関数があり、よく利用されています。しかし、Python 3.3 以降の名前空間パッケージが __init__.py を含まなくなったため、注意が必要です。

これもパッケージ配布の機能が標準で提供されることによる過渡期の課題ですね。覚えておくと、移行の tips になりそうです。移行というキーワードで話題を出したのに「昔からそうだよ」と返されるとこわいです。

13章 テスト

doctest の便利な記法

空行を意図する が紹介されています。

def safe_str(s):
    """
    >>> print(safe_str(None))
    <BLANKLINE>
    >>> print(safe_str("test"))
    test
    """
    if s is None:
        return ""
    else:
        return s

その他にも便利な機能がサポートされています。doctest は、コーディングしながら一緒にテストも書ける楽しさがありますね。

  • "..." でマッチングを省略する ELLIPSIS オプション
def myrange(x):                                                                     
    """                                                                             
    >>> list(myrange(10))  # doctest: +ELLIPSIS                                     
    [0, 1, ... 8, 9]                                                                
    """                                                                             
    return range(x)   
  • 空白文字類を無視する NORMALIZE_WHITESPACE オプション
def get_double_range(x):                                                         
    """                                                                          
    >>> get_double_range(10)  # doctest: +NORMALIZE_WHITESPACE                   
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],                                             
     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])                                             
    """                                                                          
    r = list(range(x))                                                           
    return r, r       

doctest で ELLIPSIS や NORMALIZE_WHITESPACE オプションを使うと、PEP8 の 80 文字制限も遵守できそうでこわいです。

後半の章

後半は主にアプリ開発やツール・ライブラリの紹介です。

  • 14章 Webプログラミング
  • 15章 学術/分析系ライブラリ
  • 16章 マルチメディア
  • 17章 ネットワーク
  • 18章 データストア
  • 19章 運用/監視

本当はいろいろ使ったり、開発したりする上でのコメントも入れたかったのですが、余裕がなくてそこまで至りませんでした。こうやって体系的にみると、いろんな分野で Python が使えるのが分かります。取りあえずで Python を始めてみても、後からあるドメインの処理のために別言語やツールを使わないといけない、といったケースは少ないかもしれません。

2012-04 に Hacker News で好きな言語として Python が選ばれていました。

書くことないから他の記事を引用しているというのが Python サポーターズにばれてないか、こわいです。

正誤表

最後に私が見つけた誤植等をまとめておきました。誰でも編集できるので皆さんも利用して共有してください。Python サポーターズへもこのサイトを連絡済みなので次刷で修正されると思います。

くれぐれも誤植を見つけても悪いのはネコなので文句を言ってはいけません。どんなこわいことがあるか分かりませんからね。

誤植があったらネコのせいです

emerge cat / bengal: 本を書いたらネコがクンスカした

まとめ

無理に Python3 を使う必要はなく、頑に Python2 を使う必要もないのがいまの状況な気がします。新しく開発するものでサードパーティのライブラリがあるなら Python3 で開発を始める時期に差し掛かっていると言えます。

2008年の12月にリリースしたのが Python 3.0 です。
(中略)
リリースから大凡4年がたち、2012年9月にはバージョン 3.3 になっています。

実は前バージョンの 2.0 がリリースされた後、実際に誰もが当然 2 系を使うようになるまで 5 年程度かかっています。各種サードパーティのライブラリが出そろうだけでも多くの時間を要します。Python の原作者である Guido van Rossam 氏は、今回の 3 系に関しても5年かかるだろう、と予測しています。

「2013年 Pythonの本格的な浸透」といった記事も見ましたが、着実に Python3 への移行は進んでいます。Ubuntu の次の LTS では Python3 しか提供しないという噂も聞いたりします。

本書を読んで Python3 プログラミングを始める人が増えて、日本でも Python コミュニティやその開発が活発になると嬉しいです。

Python サポーターズは、自分はこわくないですとみんな言いますが、本当はこわいと思います。

リーダブルコード

うろ覚えですが、学生の頃、プログラミングの勉強をしていて、どこかのサイトに 1000 行以上のコードを書いてみるとその言語のプログラミングの雰囲気が分かるといった内容が書いてあったような気がします。本書は特定の言語に依存した内容ではありませんが、少しは言語依存の内容もあります。

言語仕様を学び、コーディングに慣れてきたら、その次に気になるのが設計だったり、抽象化だったり、人間にとって自然な概念や分かりやすさを表現したくなってきます。本書はそういった人間のための「リーダブルコード」を書くためのコツやその具体例が簡潔にまとめられています。リーダブルコードを書こうと意識するのもまた違ったプログラミングの醍醐味に触れる機会となるでしょう。

個人的におもしろかったところ、気付きがあったところを抜粋して紹介します。

名前重要、超重要!

面倒だからと言って汎用的な名前に逃げない

tmp・retval・foo のような名前をつけるのは、「名前のことなんて考えていません」と言っているようなものだ。このような「空虚な名前」をつけるのではなく、エンティティの値や目的を表した名前を選ぼう。

がーん。

出鼻からくじかれた気分になります。普段から気を付けていることですが、たまに使ってるときもあったりします。私は名前を付けるのが下手で、リファクタリングで名前を変えたりもちょくちょくします。まさに名前付けに王道なしって気がしました。

範囲指定の名前の付け方

これは英語ネイティブではない日本人だと分からない感覚です。素直にそのまま従うと良さそうです。本書では、実際に境界値を含む含まないを図解で解説しているのでさらに分かりやすいです。

  • 限界値を含めるときは min と max を使う
  • 範囲を指定するときは first と last を使う
  • 含有/排他的範囲には begin と end を使う
式を分割する変数の意義
  • 説明変数: 式の意味を説明する変数
  • 要約変数: 式を説明する必要がない場合でも概念を簡潔に表現する変数

この概念が私にとっては目から鱗でした。

式や関数呼び出しを、そのまま関数の引数に渡したり、戻り値として返すコードをよく書いたりしていました。ローカル変数に代入すべきかどうかを迷うときもよくあったのですが、説明したり要約したりする必要があるかどうかで1つの判断基準ができました。

スコープの意識

スコープが小さければ、短い名前でも良い

以前「名前に迷ったら長い名前を書けば良い」というプラクティスを実践していて、何でも長い名前を付けていたときがありました。

ほんの数行から十数行程度の小さなユーティリティ関数なら、関数名で分かりやすさを表現すれば、内部のローカル変数は短い名前でも良いように私も同意します。Python なら PEP8 を守って 80 行文字以内に収めようとするので、以下のようなコードは状況により名前を変えたりもします。

match = re.search(REGULAR_EXPRESSION, text)
if match:
    ...

m = re.search(REGULAR_EXPRESSION, description_to_be_explained_in_detail)
if m:
    ...
Javascript の「プライベート」変数のサンプル

スコープは、言語によって違うので、その言語の仕様やイディオムを理解しておく必要があります。慣れた言語のスコープが頭の中に残ってしまうので、Javascriptクロージャーを用いたサンプルコードの奇妙さからスコープの配慮の重要性が分かりやすかったです。こういったイディオムは、より深く言語を理解する上での取っ掛かりになりますね。

var submit_form = (function () {
    var submitted = false;
    return function (form_name) {
        if (submitted) {
            return ; // 二重投稿禁止
        }
        ...
        submitted = true;
    };
}());

コメントの書き方

私が唯一意識しているコメントの書き方は「意図を表す内容を書く」ということだけでした。本書では、それ以外にも良さそうな実例が紹介されています。

コメントを書かないモチベーション

優れたコード > ひどいコード + 優れたコメント

おもしろい表現です。ひどいコードを説明するのに労力をかけるなら、ちゃんとしたコードを書きなさいという教訓ですね。

ひどいコードにはマーキングする

さらにコードの修正を促すコメントを付けることも恥ずかしがってはいけないとあります。確かに他人のコードを修正するのはやや躊躇してしまうこともあるので、堂々とコードが汚いことを認めるのが良さそうです。よく使う記法です。XXX って危険という意味だったのですね、私は知りませんでした。

記法 典型的な意味
TODO あとで手をつける
FIXME 既知の不具合があるコード
HACK あまりキレイじゃない解決策
XXX 危険!大きな問題がある
分からなかったらググれ

以下のコメントもおもしろいです。

// ベクタのメモリを解放する (「STL swap 技法」で検索してみよう)
vector().swap(data);

まとめ

名前を分かりやすくすること、コードをキレイに保つこと。

リーダブルコードの最初の取っ掛かりです。たくさんコードを書いて、失敗して、工夫して、もっと良い方法を学ぶ。本書は、シンプルなテクニックを紹介していますが、多くのプログラマーの試行錯誤の上で生き残った実践的なテクニックだと思います。知っていることであっても、普段から注意してコーディングしているか、実践しているかを考えるきっかけにも良いと思います。

Subversion で svn:externals (外部参照) を設定する

Subversion は、シンボリックリンクをサポートしています。例えば、あるユーティリティモジュールがあり、それを複数のプログラムから使いたい場合、シンボリックリンクを使うと便利です。

シンボリックリンクの作成

試してみましょう。テスト用のリポジトリを作成して Subversion サーバーを起動します。

$ svnadmin create svn-repos
$ vi svn-repos/conf/svnserve.conf 
[general]
anon-access = write  # 匿名書き込みアクセスを許可する
$ svnserve --daemon --foreground --root svn-repos/

サーバーが起動しました。クライアントからチェックアウトします。

$ svn co svn://localhost/ ./myrepo
リビジョン 0 をチェックアウトしました。

$ # 初期ディレクトリとシンボリックリンクを作成
$ cd myrepo
$ mkdir utils tool1 tool2
$ vi utils/myfunc.py
print "do something"
$ (cd tool1; ln -s ../utils/myfunc.py)
$ (cd tool2; ln -s ../utils/myfunc.py)
$ ls -l tool1 tool2
tool1:
lrwxrwxrwx 1 t2y t2y 18  98 12:00 myfunc.py -> ../utils/myfunc.py
tool2:
lrwxrwxrwx 1 t2y t2y 18  98 12:00 myfunc.py -> ../utils/myfunc.py

$ svn add utils tool1 tool2
A         utils
A         utils/myfunc.py
A         tool1
A         tool1/myfunc.py
A         tool2
A         tool2/myfunc.py

$ svn commit -m "added"
追加しています              tool1
追加しています              tool1/myfunc.py
追加しています              tool2
追加しています              tool2/myfunc.py
追加しています              utils
追加しています              utils/myfunc.py
ファイルのデータを送信しています ...
Committed revision 1.

# リポジトリの属性を調べる
$ svn proplist -R .
'tool1/myfunc.py' の属性:
  svn:special
'tool2/myfunc.py' の属性:
  svn:special

シンボリックリンクを作成すると、svn:special という属性が設定されます。

別のクライアントからチェックアウトしてみましょう。

$ svn co svn://172.16.55.131/ ./myrepo2
A    myrepo2/tool2
A    myrepo2/tool2/myfunc.py
A    myrepo2/utils
A    myrepo2/utils/myfunc.py
A    myrepo2/tool1
A    myrepo2/tool1/myfunc.py
リビジョン 1 をチェックアウトしました。

$ cd myrepo2/
$ ls -l tool1 tool2
tool1:
lrwxrwxrwx 1 t2y t2y 18  98 12:09 myfunc.py -> ../utils/myfunc.py
tool2:
lrwxrwxrwx 1 t2y t2y 18  98 12:09 myfunc.py -> ../utils/myfunc.py
$ cat tool1/myfunc.py 
print "do something"

ちゃんとシンボリックリンクが有効になっていますね。

svn:externals (外部参照) 属性の設定

Unix 系 OS であれば、これで大丈夫ですが、Windows ユーザーも使いたいときに課題があります。

But that doesn't in any way limit the usability of working copies on systems such as Windows that do not support symlinks. On such systems, Subversion simply creates a regular text file whose contents are the path to which the original symlink pointed. While that file can't be used as a symlink on a Windows system, it also won't prevent Windows users from performing their other Subversion-related activities.

シンボリックリンクのバージョン管理

Windowsシンボリックリンクをサポートしていないため、リンク先を指すテキストファイルを作成して、ユーザーの Subversion 操作においては邪魔しないとあります。

実際に Windows 環境でチェックアウトしてみましょう。

チェックアウト結果をみると、ファイルをコピーして作成してくれているように見えますが、ファイルの中身は "link ../utils/myfunc.py" です。ドキュメント等であれば良いですが、ユーティリティモジュールなどでは使えないですね。

そんなとき、Windows 環境でもシンボリックリンクのように扱える仕組みが svn:externals 属性です。

Fortunately, Subversion provides support for externals definitions. An externals definition is a mapping of a local directory to the URL―and ideally a particular revision―of a versioned directory. In Subversion, you declare externals definitions in groups using the svn:externals property. You can create or modify this property using svn propset or svn propedit (see the section called “Manipulating Properties”). It can be set on any versioned directory, and its value describes both the external repository location and the client-side directory to which that location should be checked out.

svn:externals 属性のドキュメント

svn:externals のドキュメントによると、リポジトリ内に外部リポジトリの URL を指定したり、そのリポジトリ特定バージョンを取得するように設定できるとあります。もともとは外部のリポジトリを取り組むための機能のようですが、同じリポジトリ内においても利用できます。そして、svn propsetsvn propedit で設定できると記述されていますが、残念ながら実際の設定コマンドのサンプルがここでは記述されていません。そのため、どうコマンドを実行して良いか全く分かりません (> <)

svn:externals の設定は直感的に分かりにくいので備忘録としてまとめておきます。また、Suversion 1.4 でディレクトリ単位、1.6 からファイル単位の外部参照ができるようになったので svn クライアントのバージョンにも注意してください。

$ mkdir tool3
$ svn add tool3
A         tool3

$ cd tool3
$ svn propset svn:externals "../utils/myfunc.py linked_myfunc.py" .
属性 'svn:externals''.' に設定しました

$ svn update
Updating '.':
Fetching external item into 'linked_myfunc.py':
A    linked_myfunc.py
外部項目をリビジョン 5 に更新しました。
リビジョン 5 です。

$ cat linked_myfunc.py 
print "do something"

$ svn commit -m "set svn:externals"
送信しています              .
Committed revision 6.

svn propset svn:externals の引数が覚えられないです (> <)
1つのやり方としては、以下のように相対パスで指定します。

$ svn propset svn:externals "カレントディレクトリからの相対パス  取得後のファイル名" 属性設定したいディレクトリ

svn:externals 属性は、ディレクトリに対して設定されます。

$ svn proplist tool3/linked_myfunc.py 
$ svn proplist tool3
'tool3' の属性:
  svn:externals

別のやり方として、リポジトリのルートパスからの相対パスを指定する方法もあります。今度は同じファイル名 myfunc.py で取得しています。

$ svn propset svn:externals "^/utils/myfunc.py myfunc.py" tool4
属性 'svn:externals''tool4' に設定しました
$ svn update tool4/
Updating 'tool4':
Fetching external item into 'tool4/myfunc.py':
リビジョン 7 の外部項目です。

リビジョン 7 です。
$ svn commit -m "set svn:externals"
送信しています              tool4
Committed revision 8.

Windows 環境でもチェックアウトしてみます。

Unix 系 OS 環境と同様にファイル本体がチェックアウトできました!

外部リポジトリ特定ディレクトリを取得したいときは URL を指定します。こういうときは取得した後のディレクトリ名を変更できると便利です。

$ mkdir svn
$ svn add svn/
A         svn
$ svn propset svn:externals "http://svn.apache.org/repos/asf/subversion/trunk/tools/examples/ tool-example" svn/
属性 'svn:externals''svn' に設定しました

$ svn update
Updating '.':

Fetching external item into 'tool3/linked_myfunc.py':
リビジョン 10 の外部項目です。

Fetching external item into 'tool4/myfunc.py':
A    tool4/myfunc.py
外部項目をリビジョン 10 に更新しました。

Fetching external item into 'svn/tool-example':
A    svn/tool-example/svnserve-sgid.c
A    svn/tool-example/svnput.c
A    svn/tool-example/dumpprops.py
A    svn/tool-example/svnlog2html.rb
A    svn/tool-example/check-modified.py
A    svn/tool-example/svnlook.py
A    svn/tool-example/svnshell.py
A    svn/tool-example/geturl.py
A    svn/tool-example/getlocks_test.c
A    svn/tool-example/info.rb
A    svn/tool-example/svnlook.rb
A    svn/tool-example/get-location-segments.py
A    svn/tool-example/headrev.c
A    svn/tool-example/putfile.py
A    svn/tool-example/revplist.py
A    svn/tool-example/svnshell.rb
A    svn/tool-example/minimal_client.c
A    svn/tool-example/getfile.py
A    svn/tool-example/blame.py
A    svn/tool-example/SvnCLBrowse
A    svn/tool-example/testwrite.c
外部項目をリビジョン 1382235 に更新しました。

リビジョン 10 です。

svn update を実行する度に fetch 処理が実行されます。仕方ないですが、外部リポジトリを参照すると実行に時間がかかるので、更新をチェックしたくないときは "--ignore-externals" オプションを指定します。

$ svn update
Updating '.':
Fetching external item into 'tool3/linked_myfunc.py':
リビジョン 11 の外部項目です。
Fetching external item into 'tool4/myfunc.py':
リビジョン 11 の外部項目です。
Fetching external item into 'svn/tool-example':
リビジョン 1382235 の外部項目です。

リビジョン 11 です。

$ svn update --ignore-externals
Updating '.':
リビジョン 11 です。

あまり頻繁に使う機能ではありませんが、使いたいときに実際の設定コマンドのサンプルを見つけられなくて苦労しました。

ちなみに Subversion もいまや Apache プロジェクトのトップレベルプロジェクトなんですね。ホームページも刷新されていて驚きました。

2012/9/10 追記

同じディレクトリに複数ファイルの svn:externals 属性を設定するには、svn propedit を使わないと設定できないようです。

$ svn propedit svn:externals tool5 --editor-cmd=vim  # vim エディタで複数ファイルを設定
^/utils/myfunc.py myfunc.py
^/utils/myfunc2.py myfunc2.py

属性 'svn:externals' の新しい値を 'tool5' に設定しました