python3 対応のメモ

レイトレーシング 2018春 えらいこっちゃ で、自宅のpython環境のデフォルトがpython2からpython3に上がってしまいました。

これをきっかけに、これまで書いたpython2のソースのいくつかを python3 対応してみたので、自分用のメモです。 (自分のコードの癖に合わせた対策メモです)

ぱっと思い出す大きなのは


関数の引数のアンパックが出来なくなった問題

引数のアンパック廃止は、とにかくインパクトが大きかったです。

dict( map( lambda (k, v): (xxx, yyy), dic.itmes() ) )

とか多用してましたので。

for文のアンパックは安泰なので

d = {}
for (k, v) in dic.items():
	d[xxx] = yyy

とか

dict( [ (xxx, yyy) for (k, v) in dic.items() ] )

は無事なのが悔しいところ。

とにかくmap()を多用してたので、 えらいこっちゃ v15.patch では、ユーテイリティ用の ut.py に真っ先に

 
  :
+map_lst = lambda f, lst: list( map( f, lst ) )
+map_up = lambda f, lst: map( lambda a: f(*a), lst )
+map_up_lst = lambda f, lst: list( map_up( f, lst ) )
+
+filter_lst = lambda f, lst: list( filter( f, lst ) )
+filter_up = lambda f, lst: filter( lambda a: f(*a), lst )
+filter_up_lst = lambda f, lst: list( filter_up(f, lst) )
+
  :

を用意してました。

_up は「アンパック」です。

例えば先の

dict( map( lambda (k, v): (xxx, yyy), dic.itmes() ) )

dict( ut.map_up( lambda k, v: (xxx, yyy), dic.itmes() ) )

に書き換えてまわりました。


map, filter, zipがリストを返さなくなった問題

よくよく見ればそのままのコードで python3 の恩恵にあずかれるパターンもあったりします。

とにかく python3 でも動けばよしとするならば、目をつむって

map(...) --> ut.map_lst(...)

ut.map_up(...) --> ut.map_up_lst(...)

に書き換えです。

機械的に書き換え作業してると、なかなか気付かずにハマった箇所がありました。

パックマンもどき2017秋 クロージャと空クラス で書いた「継承もどき」

def cls_to_dic(c):
	ks = filter( lambda k: not k.startswith('_'), dir(c) )
	return dict( map( lambda k: ( k, getattr(c, k) ), ks ) )

クラスのアトリビューとで '_' で始まるもの以外を、辞書に。


def dic_to_cls(d, c=None):
	if not c:
		c = Empty()
	map( lambda (k, v): setattr(c, k, v), d.items() )
	return c

辞書を受け取って、クラスのアトリビュートに上書き。


def extends(pcls, c=None):
	if not c:
		c = Empty()
	dic_to_cls( cls_to_dic(pcls), c )
	return c

cls_to_dic()側のmapは結果のイテレータをdict()で回すので問題なくて、 問題はdic_to_cls()側のmap。

ここでは lambda式の返す値はどうでもよくて、setattr()の副作用というか、 クラスcのアトリビュートを設定する事だけを目的にしてます。

なまじ lambda で引数のアンパックを使っているので、 そちらに目がいってしまって

	map( lambda (k, v): setattr(c, k, v), d.items() )

	ut.map_up( lambda k, v: setattr(c, k, v), d.items() )

に書き換えてみて、さて動かない... アトリビュートが設定されてない。なんで?

map()はイテレータを返すだけです。 list()をとるなどして消費しないと回りませんでした。

まぁ、結局対応は

	ut.map_up_lst( lambda k, v: setattr(c, k, v), d.items() )

でした。


reduceはimportが必要になった

これは、新規にreduceを使うモチベーションが下がってしまいました。

既に使ってるところは、 v15.patch

  :
--- rt_v14/lstx.py	2018-03-31 17:25:01.000000000 +0900
+++ rt_v15/lstx.py	2018-04-02 18:13:19.000000000 +0900
@@ -1,5 +1,6 @@
 #!/usr/bin/env python
 
+from functools import reduce
 import ut
 import v
 import line

などと追加です。


strとbytes

python2 でstr(文字列)が返ってたいくつかの箇所が、 python3 でbytesが返るようになり、怒られてたはずです。

例えば v24.patch

 def imgs_to_video(img_name, video_name, fps, zm):
 	cmd = 'ls {}[0-9]*.jpg'.format(img_name)
-	lst = ut.cmd_exec(cmd).strip().split('\n')
+	lst = ut.cmd_exec(cmd).decode().strip().split('\n')
 

では、decode()でstrに戻す対応を入れました。

ut.py
  :
def cmd_exec(cmd):
	try:
		return subprocess.check_output(cmd, shell=True)
	except:
		return ''
  :

subprocess の実行結果の文字列は python3 で str から bytes に変更

python3 の環境では

>>> s = ' foo.bar  '
>>> type(s)
<class 'str'>

>>> b = s.encode()
>>> type(b)
<class 'bytes'>

>>> s2 = b.decode()
>>> type(s2)
<class 'str'>
>>> s2
' foo.bar  '

>>> len(s)
10
>>> len(b)
10

len()は問題なし

>>> s.strip()
'foo.bar'
>>> b.strip()
b'foo.bar'

strip()も使える

>>> s.strip().split('.')
['foo', 'bar']
>>> b.strip().split('.')
Traceback (most recent call last):
  File "<stdin>", line 1, in 
TypeError: 'str' does not support the buffer interface

bytes の split(xxx) は引数がbytesでないとダメか

>>> b.strip().split( '.'.encode() )
[b'foo', b'bar']

socket の sendall() の引数や recv() の返り値も、同様です。 pthon3 から bytes です。


バイナリファイル

python2 では、バイナリファイルを open(fn, 'r') して read() すれば、strとして読めました。

例えば

$ which cat
/bin/cat

$ head /bin/cat > /tmp/f.bin

$ hd /tmp/f.bin | head
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 01 00 00 00  c2 9e 04 08 34 00 00 00  |............4...|
00000020  c4 b2 00 00 00 00 00 00  34 00 20 00 09 00 28 00  |........4. ...(.|
00000030  1c 00 1b 00 06 00 00 00  34 00 00 00 34 80 04 08  |........4...4...|
00000040  34 80 04 08 20 01 00 00  20 01 00 00 05 00 00 00  |4... ... .......|
00000050  04 00 00 00 03 00 00 00  54 01 00 00 54 81 04 08  |........T...T...|
00000060  54 81 04 08 13 00 00 00  13 00 00 00 04 00 00 00  |T...............|
00000070  01 00 00 00 01 00 00 00  00 00 00 00 00 80 04 08  |................|
00000080  00 80 04 08 d8 a4 00 00  d8 a4 00 00 05 00 00 00  |................|
00000090  00 10 00 00 01 00 00 00  08 af 00 00 08 3f 05 08  |.............?..|

$ python2
>>> f = open( '/tmp/f.bin', 'r' )
>>> s = f.read()

>>> type(s)


>>> len(s)
6781

>>> s8 = s[:8]
>>> s8
'\x7fELF\x01\x01\x01\x00'

python3 では

$ python3
>>> f = open( '/tmp/f.bin', 'r' )
>>> s = f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.4/codecs.py", line 319, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 61: invalid start byte

read でエラー

>>> fb = open( '/tmp/f.bin', 'rb' )

'rb' で open すれば大丈夫で

>>> b = fb.read()
>>> type(b)
<class 'bytes'>

bytes で読めます

>>> b8 = b[:8]
>>> b8
b'\x7fELF\x01\x01\x01\x00'

「逆もまた真なり」か、どうかは別として、バイナリファイルを作るときも同様です

'wb' で open して bytes を write() します。

パックマンもどき2017秋 v7.patch で python3 対応をしました。

os.read() も str から bytes に変わってたので、 ここでは、キー操作の処理でひっかかりました。

import sys
import os 
  :
fd = sys.stdin.fileno()
s = os.read(fd, 1)

などと、sys.stdin の fd から 1文字読み出す箇所で、 strからbytes に変わってしまうので、 対応としては、os.read()結果をdecode()してbytesからstrにしてました。

また、python2 でバイナリデータをリダイレクトで stdin に与えて、 sys.stdin.read() していた場合も要注意です。

python3 で、stdin からバイナリをリードするには、 sys.stdin でなく sys.stdin.buffer を使います。

同様に、sys.stdout にバイナリを出力するには、 sys.stdout でなく sys.stdout.buffer を使えば良いはず。

import sys
import six

def read_bin(fn):
	f = ( sys.stdin.buffer if six.PY3 else sys.stdin ) if fn == '-' else open(fn, 'rb')
	bs = f.read()
	if fn != '-':
		f.close()
	return bs

def write_bin(fn, bs):
	f = ( sys.stdout.buffer if six.PY3 else sys.stdout ) if fn == '-' else open(fn, 'wb')
	f.write(bs)
	if fn != '-':
		f.close()


整数の割算

$ python2
>>> 1/3
0

$ python3
>>> 1/3
0.3333333333333333

結果がfloatに!

整数の割算は '//' との事

>>> 1//3
0

なのですが、'//' って C++のコメントのキーワードって印象が強くてどうも...

なので

$ python3
>>> int(1/3)
0

な対応をしてまわりました。python2 への後方互換もあるので。

しかしpython3のこの仕様は、これまでpython2で整数同士の割算結果が、 floatにならなくて困ってた人々が多くいたって事だろうか...?

整数のレジスタどうしで割算したら、結果の入る整数のレジスタには、整数しか返らんと考えますが... 考えが古いのでしょうか。


print

関数になったので '(' ... ')' で囲みます。

ということは、タプルにして表示したかったところが、ちょっとアッサリ風味になりますね。

$ python2
>>> print(1,2)
(1, 2)
>>> print (1,2)
(1, 2)

$ python3
>>> print(1,2)
1 2
>>> print (1,2)
1 2

になってしまうので

>>> print((1,2))
(1, 2)
>>> print ((1,2))
(1, 2)