わかっちゃいたけど後回しにしていた Lambda Python2、たまたまやる気が燃え上がったので Python3 に移行しました。
Python 自体はまぁそこそこの素人なので、詳しいことはあんま書かないで、やったこととか変更点のピックアップとかしていきたいと思います。
Python2 よや すら かに
Python2 がこんな感じで終焉を迎えるので、Lambdaの公式説明的には、段階的ではあるものの、End of life 以降は使えなくなっていくので、それまでに切り替えようねということになります。
Pythonの公式をチェックすると、Python2.7 は 2020-01-01 なので、もーさすがに頃合いというか早よやれって感じですね。
Lambda Python2.7 が 2015年10月にリリースされ、
Lambda Python3.6 が 2017年4月、
Lambda Python3.7 が 2018年11月、
で、ウチのLambdaさんは Python2.7 しかない時代から開発し始めたのと、インフラ用のコードはそうたいした内容ではないので、そのまま 2.7 で動かしてきたのですが、12月にこんなことやりたくないので、マル済にしておいた次第です。
ここからは作業内容についてまとめていきます。
Gitの作業
インフラ用のLambdaコードは、Git で管理していて、master に merge したら CI が zip にして S3 にアップロードしてくれる、ようにしています。zip はメインコードと、ライブラリなどのレイヤーに分けることで、更新時の無駄な転送などを減らしています。その構成をそのままに、Python3 用に新しくレポジトリを作成して、コードを丸っとコピーして、CI関連の設定もコピー&編集して準備完了です。
開発環境での作業
Python3 の準備
私は CentOS7 で作業していたので、python3 の準備から始まります。
1 |
yum install python3 python3-devel mysql-community-devel |
python3-devel は mysqlclient のコンパイルと、2to3 コマンドで必要になります。
Version は Python3.6 になります。Runtime EOL 的に、3.7 にしてしまおうとも思ったのですが、CentOS8 ですらまだ標準が 3.6 なので、pyenv とかあっち系を運用手順にあまり入れたくないし、まだ2年あるしいいかなって感じです。非常に微妙なラインなので、管理者の決断任せで大丈夫だと思います。
pip3 でパッケージインストール
Lambda に色々持っていくやつを、コードに含めてしまいます。今回、MySQL-Python が Python2 で動かなくなっているので、その Fork である mysqlclient に切り替えています。
1 |
pip3 install pytz PyYAML python-memcached mysqlclient redis -t python/ |
この python/ ディレクトリは、レイヤーとして外だししています。
あと余談ですが、手元の 3.6 でインストールしたものを、lambda python 3.7 で動かそうとしたらダメでした。
libmysqlclient のコピー
mysqlclient で必要になるので、不格好ですが持っていきます。
1 2 |
yum install mysql-community-libs cp /usr/lib64/mysql/libmysqlclient.so.18 lib/ |
lib/ はコードのどこかで扱えるようにしています。
1 2 3 |
import os import sys sys.path.append(os.getcwd() + "/lib") |
2to3-3 コマンドでコード変換
2to3-3 で .py を指定すると、変換の diff を確認できます。オプションで -w をつけると、実際に変換して、かつ元のファイル名 + .bak でオリジナルを保管してくれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# まずは1ファイルで確認してみる 2to3-3 example.py # 一気に見たり 2to3-3 *.py | less # ディレクトリごとも可 2to3-3 modules/ | less # 問題なければガバっと変換する 2to3-3 -w modules/ # 元のコードは別にあるだろうから、.bak は削除 rm modules/*.py.bak |
どーでもいいですけど、2to3 ってだいぶ傲慢な名付けですよね…… せめて py2to3 とかにすればいいのに:-(
diff ピックアップ
python2 と 3 の違いは、ググってもらうとして、ウチの場合はこんな変更点が出ましたよという、例でも出しておきます。ぶっちゃけ、たいしたことなかったです:-)
1 2 |
- print "<Tables>" + print("<Tables>") |
list ラップ
主に for での変数の扱いにおいて、list() で囲む必要があります。
1 2 3 4 5 6 7 8 9 |
- for key,info in tables.items(): + for key,info in list(tables.items()): - for service in price_list["offers"].keys(): + for service in list(price_list["offers"].keys()): - return self.accounts.keys() + return list(self.accounts.keys()) |
not in
has_key よりは not in の方が簡単だし好みではある。
1 2 |
- if not values["services"].has_key(service_name): + if service_name not in values["services"]: |
文字列の型
これは自動変換してくれましたが、してくれない系もあってあとで手こずります。
1 2 3 4 5 6 |
- if isinstance(k, unicode): dict[k.encode('utf-8')] = v + if isinstance(k, str): dict[k.encode('utf-8')] = v - if isinstance(id, basestring): #Value + if isinstance(id, str): #Value |
8進数
書き方が変わりました。
1 2 |
- os.chmod(file, 0777) + os.chmod(file, 0o777) |
lambdaいらず
この filter よりは for in の方が綺麗ですね。っていうか、こんな変換してくれるのは凄い。
1 2 3 |
res = ec2.describe_images(Owners = ['self']) - images = filter(lambda x: re.match(r"^%s.*" % image_prefix, x['Name'], re.IGNORECASE), res['Images']) + images = [x for x in res['Images'] if re.match(r"^%s.*" % image_prefix, x['Name'], re.IGNORECASE)] |
urllib
そうなりましたか、くらい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-import urllib +import urllib.request, urllib.parse, urllib.error - url_open = urllib.urlopen(report_url) + url_open = urllib.request.urlopen(report_url) - req = urllib2.Request(json_url) + req = urllib.request.Request(json_url) - if params is not None: url += "?%s" % urllib.urlencode(params) + if params is not None: url += "?%s" % urllib.parse.urlencode(params) |
引っかかったところ
2to3 のあとに一通り動作確認をしていって、残ったエラーを修復していきます。MySQL-Python -> mysqlclient
さきほども書きましたが、pip3 install の時点で MySQL-Python のインスコで、こんなエラーが出て失敗します。これを mysqlclient に変更することで、コードの変更はゼロで対応完了となります。
文字列の str と bytes
文字列型の扱いが変わったことで、変換を求められます。例えば、struct でこんなエラーが出たので、引数の文字列に b をつけたり、結合時に型を合わせるために変換したり、
1 2 3 4 5 6 |
def __pack(self, request): string = json.dumps(request) - header = struct.pack('<4sBQ', 'ZBXD', 1, len(string)) - return header + string + header = struct.pack(b'<4sBQ', b'ZBXD', 1, len(string)) + return header + string.encode() |
POSTデータを bytes に変換するなど。
1 2 |
- request = urllib2.Request(self.url, data, self.headers) + request = urllib.request.Request(self.url, data.encode(), self.headers) |
さすがに処理に使う変数の型は、自動変換で見えるはずがないので、struct や sock など bytes での取り扱いになった系を把握して、片っ端から対応していくことになります。
と、まぁ思ったよりスンナリ終わったので拍子抜けしました。インフラ用コードがたかが知れてるってのもありますけど。
Lambda の Python を書いている時は、とても心が安らぐので、また数年お世話になって、また数年後に更新するくらいは、気持ちよくやっていきたいと思いマッスル!!