概要
このハンズオンでは、Python 2.x のコードを 3.x に移植するときの簡単な手順を体験します。願わくば午前中に終わらせて、午後は自由に時間を使ってもらうつもりでいます。
使用するソフトウェア
- Python 2.6
- Python 3.1
- pyhack3py3ex.zipをダウンロード
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, inmain()
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!!
メモ
参考資料
記事や解説
- Python 3 Porting /Co-Existence Resources
- Python Tutorial 3.1.1
- Porting your code to Python3 (PDF)
- Python 3 への移植: すべきこととすべきでないこと
- Dive into Python - APPENDIX A Porting Code to Python 3 with 2to3
- Porting SimpleTAL to Python 3
- Python 3 porting workflow?
- Python Python Python (aka Python 3)
- Porting Python Code to 3.0 (PythonInfo Wiki)
- Dive Into Python ch 15
- porting setuptools to py3k
- 2to3
- Porting Durus/QP/Qpy to py3k
- Porting Django to 3k
ツール
- Python Incompatibility tests
- 2to3 Automated Python 2 to 3 translation manual
- lib3to2 lib3to2 is a set of fixers to refactor code written for Python 3.x into code that can run on a 2.x interpreter.
Python 3 用ライブラリの開発プロジェクト
- cherrymuffin_py3
- virtualenv3
- youtube-dl
- pip3
- vlastic Vlastic is a pretty RESTful web framework for Python 3.0. It (is going to) implements WSGI interface.
- eenvoudig Python 3 Web Framework modeled after Sinatra
- mysql-python
- SQLAlchemy
- Beautiful Soup
- PyPdf
- Universal Feed Parser
- python-oauth
- PyPI
- Python 3 Porting /Co-Existence Resources