2010-01-22

Python 3 ハンズオン資料 for Python Hack-a-thon #3

概要



このハンズオンでは、Python 2.x のコードを 3.x に移植するときの簡単な手順を体験します。願わくば午前中に終わらせて、午後は自由に時間を使ってもらうつもりでいます。





使用するソフトウェア





Python 3 のポイント
別の勉強会(BP Study)で使った資料



2.x 版で動作確認



foo.py は Yahoo! の検索 API の結果を受け取って、テキストファイルに保存するプログラムです。そして tests.py は、一部の昨日のユニットテストです。これらが動作することを確認してみましょう。



$ python2.6 2/tests.py
...
----------------------------------------------------------------------
Ran 4 tests in 0.330s

OK
$ python2.6 2/foo.py
$


2to3 の実行



Python 3.x には 2to3 という変換ツールが付属します。これを使って、コードを変換してみましょう。



$ mkdir 3
$ cp -r 2/*.py 3/.
$ 2to3-3.1 -w 3/
RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
RefactoringTool: Refactored 3/foo.py
--- 3/foo.py (original)
+++ 3/foo.py (refactored)
@@ -1,10 +1,10 @@
# -*- encoding: utf-8 -*-
-import urllib
+import urllib.request, urllib.parse, urllib.error
from xml.dom.minidom import parseString
from utils import get_text

def main():
-    query = u'ほげ'
+    query = 'ほげ'
     count = 10
     response = open_yahoo(query, count).read()
     dom = parseString(response)
@@ -18,10 +18,10 @@
def open_yahoo(query, count):
     api = 'http://search.yahooapis.jp/WebSearchService/V1/webSearch'
     appid='CDU9keOxg67iAhAcNjcEjZbj25HFcV2DkA62bAxhQn_4FUoPJN2lFQdP5MfnIksh75e650BR.A--'
-    params = urllib.urlencode({'appid':appid,
+    params = urllib.parse.urlencode({'appid':appid,
                                'query':str(query.encode('utf-8', 'ignore')),
                                'result':count})
-    response = urllib.urlopen(api+'?'+params)
+    response = urllib.request.urlopen(api+'?'+params)
     return response

class Result:
RefactoringTool: Refactored 3/tests.py
--- 3/tests.py (original)
+++ 3/tests.py (refactored)
@@ -14,8 +14,8 @@
                          get_text(dom.getElementsByTagName('bar')[0].childNodes))

     def testJp(self):
-        dom = parseString(u"

こんにちは

".encode('utf-8'));
-        self.assertEqual(u"こんにちは",
+        dom = parseString("

こんにちは

".encode('utf-8'));
+        self.assertEqual("こんにちは",
                          get_text(dom.getElementsByTagName('bar')[0].childNodes))

class AskYahooTest(unittest.TestCase):
RefactoringTool: No changes to 3/utils.py
RefactoringTool: Files that were modified:
RefactoringTool: 3/foo.py
RefactoringTool: 3/tests.py
RefactoringTool: 3/utils.py


ここで 2to3 がやっていることは以下のとおりです。



  • u'...' → '...'


  • print 文 → print 関数


  • urllib の階層変更


3.x 版のユニットテスト



では 3.x 版のユニットテストをしてみましょう。



$ python3.1 3/tests.py
FF..
======================================================================
FAIL: test (__main__.AskYahooTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "3/tests.py", line 29, in test
    self.assertEqual(count, len(dom.getElementsByTagName('Result')))
AssertionError: 10 != 7

======================================================================
FAIL: testURL (__main__.AskYahooTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "3/tests.py", line 36, in testURL
    'http://search.yahooapis.jp/WebSearchService/V1/webSearch?query=yahoo&result=10&appid=CDU9keOxg67iAhAcNjcEjZbj25HFcV2DkA62bAxhQn_4FUoPJN2lFQdP5MfnIksh75e650BR.A--')
AssertionError: 'http://search.yahooapis.jp/WebSearchService/V1/webSearch?query=b%27yahoo%27&result=10&appid=CDU9keOxg67iAhAcNjcEjZbj25HFcV2DkA62bAxhQn_4FUoPJN2lFQdP5MfnIksh75e650BR.A--' != 'http://search.yahooapis.jp/WebSearchService/V1/webSearch?query=yahoo&result=10&appid=CDU9keOxg67iAhAcNjcEjZbj25HFcV2DkA62bAxhQn_4FUoPJN2lFQdP5MfnIksh75e650BR.A--'

----------------------------------------------------------------------
Ran 4 tests in 0.986s

FAILED (failures=2)




2つテストが失敗しています。ひとつめは結果のエントリ数が10であるはずが、7件しか返ってきていないこと。もうひとつは、URL として生成した文字列が一致していないことです。



実は、この場合は URL が間違っているので、件数が期待通りになりません。



3.x 版のユニットテストを通す



    fout = file("result.txt", "w")
    for result in results:
        fout.write(("%s\n" % result.url).encode("utf-8"))
        fout.write(("%s: %s\n"%(result.title, result.summary)).encode("utf8"))
        fout.write("\n")


Python 2.6 では、urlencode() 関数に与える辞書の value は、ASCII 文字列 str 型でなければなりません。なので、このコードでは明示的にエンコードしています。



一方、Python 3.1 で、.encode() メソッドの戻り値は bytes 型というバイトシーケンスです。それを str() すると 「b'...'」のようなユニコード文字列に変換されます。前後に不必要な文字がついてしまうのです。また 3.1 の urlencode() 関数はユニコード文字列を受け取ると、UTF-8 で自動的にエンコードします。



というわけで、ここは 2.x と 3.x の非互換な関数を使っていますので、コードを修正しましょう。



    fout = open("result.txt", "w", encoding="utf-8")
    for result in results:
        fout.write("%s\n" % result.url)
        fout.write("%s: %s\n"%(result.title, result.summary))
        fout.write("\n")


で、テスト実行。


$ python3.1 3/tests.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.474s

OK


テストがとおりました。オレ、できたよ、かーちゃん。



3.x 版の動作確認



ではアプリのほうを実行してみましょう。



$ python3.1 3/foo.py
Traceback (most recent call last):
  File "3/foo.py", line 36, in

    main()
  File "3/foo.py", line 12, in main
    fout = file("result.txt", "w")
NameError: global name 'file' is not defined



がーん。またもやエラーです。Python 3.x では file() 関数が廃止され、open() 関数を使うことになっています。というわけで foo.py を以下のように修正します。ついでにエンコード方法を指定しておきましょう。



    fout = open("result.txt", "w", encoding="utf-8")  # ここ


これでどうでしょう。



$ python3.1 3/foo.py
Traceback (most recent call last):
  File "3/foo.py", line 36, in


    main()
  File "3/foo.py", line 14, in main
    fout.write(("%s\n" % result.url).encode("utf-8"))
TypeError: must be str, not bytes





またもやエラーです。fout はテキストモードで開いているので、write するときには必ず str つまりユニコード文字列を渡さないといけません。




fout.write("%s\n" % result.url)
fout.write("%s: %s\n"%(result.title, result.summary))
fout.write("\n")


これで完成です。



というわけで...



What's New in Python 3.x のガイドのとおりの手順で、2.x から 3.x への移植作業をしてみました。ガイドにも書かれているように、ユニットテストは重要です。2to3 はそこそこやってくれますが、いろいろと細かい修正作業が必要で、とくに文字列周りはアプリケーションが何をしているのか知らないといけません。



また、この例ではサードパーティのライブラリを使っていません。Django のようなおおきなライブラリやフレームワークを使う場合には、ライブラリ側が 3.x 対応していないと、Python 3 への以降は難しいと思います。



さらにここではアプリケーションの移植でしたが、ライブラリの移植では工夫が必要になります。 Python 2.x と Python 3.x の両方をサポートしたいのであれば、できれば 2to3 だけで変換できるようにしておくのが得策でしょう。



というわけで、Happy Hacking!!



pyhack3py3ex.zipをダウンロード






 



メモ