pythonのちょっとしたメモ

自分でpythonのプログラムを組んでて、ちょいちょいハマる事柄を記録していきます。

2024/MAR/27

更新履歴
日付 変更内容
2020/APR/19 新規作成
2020/MAY/02 boolと整数と辞書 追加
2020/JUL/12 辞書のupdate()メソッド 追加
2020/SEP/08 varsとオブジェクトのアトリビュート 追加
2020/SEP/19 emptyモジュール落とし穴 追加
2020/SEP/20 emptyモジュールのto_attr()の効用 追加
2020/OCT/13 リストに空文字列を追加すると 追加
2020/OCT/30 SIGINT(^Cキー)で終了するか
2020/OCT/31 forの動作について
2020/DEC/29 map()のままでも良い場合
2021/JAN/12 map()のままでも良い場合 len boolの判定
2021/JAN/30 importしたファイルに定義されているグローバル変数の値
2021/FEB/11 map()のままでも良い場合 sumの引数
mapとsum
2021/MAR/04 変数や内部関数の定義順の違い
2021/MAR/19 map()のままでも良い場合 allとanyの引数
2021/MAR/22 map()のままでも良い場合 文字列のjoin
2021/JUN/30 map()のままでも良い場合 dict生成
2024/MAR/27 bytesのスライス

目次


内部関数とevalのスコープ

クロージャを多用するので、内部関数はよく使います。

pythonのユーティリティ・プログラム 2020冬empty.py の通り。

eval()が絡むとちょいちょいハマってしまうので、メモしておきます。

例えば

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar():
		a = 3
		return a

	return bar

if __name__ == "__main__":
	f = foo()
	print( f() )
# EOF
$ chmod +x foo.py

$ ./foo.py
3

a = 3 行をコメントアウトすると

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar():
		#a = 3
		return a

	return bar

if __name__ == "__main__":
	f = foo()
	print( f() )
# EOF
$ ./foo.py
2

a = 2 行もコメントアウトすると

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	#a = 2

	def bar():
		#a = 3
		return a

	return bar

if __name__ == "__main__":
	f = foo()
	print( f() )
# EOF
$ ./foo.py
1

ですね。

ところが

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s):
		a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF

の場合

$ ./foo.py
3

a = 3 行をコメントアウトすると

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s):
		#a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF
$ ./foo.py
1

!!! なんと!

そして

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	#a = 2

	def bar(s):
		#a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF
$ ./foo.py
1

a = 2 のスコープはeval()に見えてないのですね。

execでは?

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s):
		a = 3
		exec( s )

	return bar

if __name__ == "__main__":
	f = foo()
	f( 'print( a )' )
# EOF
$ ./foo.py
3

a = 3 行をコメントアウトすると

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s):
		#a = 3
		exec( s )

	return bar

if __name__ == "__main__":
	f = foo()
	f( 'print( a )' )
# EOF
$ ./foo.py
1

a = 2 行もコメントアウトすると

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	#a = 2

	def bar(s):
		#a = 3
		exec( s )

	return bar

if __name__ == "__main__":
	f = foo()
	f( 'print( a )' )
# EOF
$ ./foo.py
1

eval()と同じですね。

例えば、引数のローカル変数で、初期値として与えたら

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s, a=a):
		#a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF
$ ./foo.py
2

a = 2 のスコープが見えます。

当然aの束縛は変更できません。

あるいは bar で一度 a = 2 を参照しておくと

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s):
		a
		#a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF
$ ./foo.py
2

これでも a = 2 見えます。

むー。もやっとしますな。

locals()を使ってみると

$ cat foo.py
#!/usr/bin/env python

a = 1

def foo():
	a = 2

	def bar(s):
		locals()[ 'a' ] = a
		#a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF
$ ./foo.py
2

いっそうの事、fooのローカル全部を持ち込むならば

$ cat foo.py
def foo():
	a = 2

	lcs = locals()
	print( lcs )

	def bar(s):
		lcs_ = locals()
		print( lcs_ )
		f = lambda kv: kv[ 0 ] not in lcs_
		add = dict( filter( f, lcs.items() ) )
		lcs_.update( add )
		print( lcs_ )
		#a = 3
		return eval( s )

	return bar

if __name__ == "__main__":
	f = foo()
	print( f( 'a' ) )
# EOF
$ ./foo.py
{'a': 2}
{'s': 'a', 'lcs': {'a': 2}}
{'s': 'a', 'lcs': {'a': 2}, 'a': 2}
2

foo()の実行で lcs に代入した時点での locals は a = 2

内部関数 bar() が返されて、bar() が呼び出されます。

bar()が呼び出されてすぐの locals() は、引数の s と、lcs。

lcsはlcs.items()として参照してるから、見えてますね。

そして、一応fooの方のlcsからキーがダブらないものだけ選んで、a = 2 を追加

eval( 'a' )で、locals() に追加した a = 2 が見えます。

空クラス empty.py を使って

$ cat foo.py
#!/usr/bin/env python

import empty

def foo_new():
	e = empty.new()
	e.a = 2

	def bar(s, e=e):
		return eval( s )

	return empty.to_attr( e, locals() )

if __name__ == "__main__":
	foo = foo_new()
	print( foo.bar( 'e.a' ) )
# EOF
$ ./foo.py
2

barの引数でeだけをとりこんでおいて、文字列側で'e.'つきで指定する。

このくらいで手討ちでしょうか。


boolと整数と辞書

すぐ忘れてしまいます。

Boolean オブジェクト
Python の Bool 型は整数のサブクラスとして実装されています。

である事を。

$ python
>>> d = { 0: 'zero', 1: 'one', True: 'True', False: 'False' }

などという辞書を作ると

>>> d.get( 0 )
'False'
>>> d.get( False )
'False'
>>> d.get( 1 )
'True'
>>> d.get( True )
'True'

なんで?

>>> list( d.keys() )
[0, 1]

0, 1の2つしかキーが無いのに

>>> True in d
True
>>> False in d
True

TrueもFalseもある?

>>> d
{0: 'False', 1: 'True'}

ああ、boolはintのサブクラス

>>> d2 = { 0: 'zero', 1: 'one' }
>>> d2
{0: 'zero', 1: 'one'}

boolが辞書のキーとして扱われるときは、int !!!

>>> d2[ False ] = 'False'
>>> d2
{0: 'False', 1: 'one'}

>>> d2[ True ] = 'True'
>>> d2
{0: 'False', 1: 'True'}

上書きされてしまい、意図しなかった辞書の出来上がり orz

辞書の「値」としては、ちゃんと区別されてるので...

>>> d3 = { 0: False, 1: True, 2: 0, 3: 1 }
>>> d3
{0: False, 1: True, 2: 0, 3: 1}

ちょいちょいひっかかります。

>>> False == 0
True
>>> False is 0
False

>>> True == 1
True
>>> True is 1
False
>>> True == 2
False

という事は、辞書のキーの判定は'is'ではなく、'=='という事ですね。


辞書のupdate()メソッド

当然ですが、階層的な再帰処理ではありません。

なのですが、YAMLデータでついつい階層的に作ってしまい、 update()メソッドで意図に反した動作になり、

「しまった...」

と、なりがちです。

通常のupdate()の動作例

>>> targ = { 1: 'a', 2: 'b' }
>>> targ
{1: 'a', 2: 'b'}
>>> add = { 2: 'B', 3: 'C' }
>>> targ.update( add )
>>> targ
{1: 'a', 2: 'B', 3: 'C'}

階層的なデータでありがちな勘違い

>>> s = '''
... foo:
...   kind: a
...   num: 2
... bar:
...   kind: b
...   num: 3
... '''
>>> s
'\nfoo:\n  kind: a\n  num: 2\nbar:\n  kind: b\n  num: 3\n'

階層的な辞書のYAMLデータを作っておいて

>>> import yaml
>>> targ = yaml.load( s )
>>> targ
{'foo': {'kind': 'a', 'num': 2}, 'bar': {'kind': 'b', 'num': 3}}

>>> print( yaml.dump( targ ) )
bar: {kind: b, num: 3}
foo: {kind: a, num: 2}

ロードして辞書に。

>>> s_add = '''
... foo:
...   num: 5
... '''
>>> add = yaml.load( s_add )
>>> add
{'foo': {'num': 5}}

fooのnumだけを2から5に更新したく、そのようなデータを作って

>>> targ.update( add )

update()

>>> targ
{'foo': {'num': 5}, 'bar': {'kind': 'b', 'num': 3}}
>>> print( yaml.dump( targ ) )
bar: {kind: b, num: 3}
foo: {num: 5}

あちゃー。

fooのkindが消えてしまいました。

再帰的なupdate処理の例

def dic_update_r(targ, add):
	for (k, v) in add.items():
		tv = targ.get( k )
		if type( v ) == dict and type( tv ) == dict:
			dic_update_r( tv, v )
		else:
			targ[ k ] = v

pythonのユーティリティ・プログラム 2020冬base.py に入れてみました。

>>> s
'\nfoo:\n  kind: a\n  num: 2\nbar:\n  kind: b\n  num: 3\n'
>>> targ = yaml.load( s )
>>> targ
{'foo': {'kind': 'a', 'num': 2}, 'bar': {'kind': 'b', 'num': 3}}

>>> add
{'foo': {'num': 5}}

update前の状態に戻して

>>> import base
>>> base.dic_update_r( targ, add )

再帰的にupdate

>>> targ
{'foo': {'kind': 'a', 'num': 5}, 'bar': {'kind': 'b', 'num': 3}}

>>> print( yaml.dump( targ ) )
bar: {kind: b, num: 3}
foo: {kind: a, num: 5}

これで無事意図した通り、fooのnumが2から5に更新されました。


varsとオブジェクトのアトリビュート

オブジェクトにvars()して、アトリビュートの辞書を取得したとき。

なぜだか「辞書はコピーだ」と、強く思い込んでおりました。

実際にはそんな事はなく、辞書を変更すると本体のオブジェクトに反映されます。

長年の間違った思い込みのせいで、驚きです!

$ python
>>> class Empty:
...     pass
...

>>> e = Empty()
>>> e.foo = 'foo'

>>> d = vars( e )
>>> d
{'foo': 'foo'}

>>> d[ 'foo' ] = 123
>>> e.foo
123

追加も?

>>> d[ 'bar' ] = 'BAR'
>>> e.bar
'BAR'

できます。

削除も?

>>> d.pop( 'foo' )
123
>>> e.foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Empty' object has no attribute 'foo'

できます。

XXXattr()関数

hasattr(), getattr(), setattr()

まぁ、あまり意味ないですが

>>> hasattr( e, 'bar' )
True

>>> 'bar' in vars( e )
True
>>> getattr( e, 'bar' )
'BAR'

>>> vars( e ).get( 'bar' )
'BAR'
>>> setattr( e, 'hoge', 'HOGE' )
>>> e.hoge
'HOGE'

>>> vars( e )[ 'fuga' ] = 'FUGA'
>>> e.fuga
'FUGA'

>>> len( dir( e ) )
28

>>> dir( e )[ -5: ]
['__subclasshook__', '__weakref__', 'bar', 'fuga', 'hoge']

>>> list( filter( lambda s: not s.startswith( '__' ), dir( e )  ) )
['bar', 'fuga', 'hoge']
>>> list( vars( e ).keys() )
['hoge', 'fuga', 'bar']


emptyモジュール落とし穴

kon_ut の empty.py

ただの空のクラスなのですが、クロージャとしてクラスの代わりに多用してきました。

例えば

import empty

def foo_new(add_v):
	d = { 'add_v': add_v, 'sum': 0 }

	def add():
		d[ 'sum' ] += d[ 'add_v' ]
		d[ 'add_v' ] += 1
		return d[ 'sum' ]

	return empty.new( locals() )

を定義して

foo = foo_new( 3 )
foo.add()
foo.add()
print( foo.add() )

を実行すると

12

辞書で状態を記録するのもアレなので

def bar_new(add_v):
	e = empty.new()
	e.add_v = add_v
	e.sum = 0

	def add():
		e.sum += e.add_v
		e.add_v += 1
		return e.sum

	return empty.to_attr( e, locals() )

などとして

bar = bar_new( 3 )
bar.add()
bar.add()
print( bar.add() )

を実行すると

12

よくやるパターンの落とし穴

では、生成時に、指定回数add()できるようにした版に

def foo_new(add_v, init_n=0):
	d = { 'add_v': add_v, 'sum': 0 }

	def add():
		d[ 'sum' ] += d[ 'add_v' ]
		d[ 'add_v' ] += 1
		return d[ 'sum' ]

	for i in range( init_n ):
		add()

	return empty.new( locals() )
foo = foo_new( 3, 2 )
print( foo.add() )
12

bar_new()も同様に

def bar_new(add_v, init_n=0):
	e = empty.new()
	e.add_v = add_v
	e.sum = 0

	def add():
		e.sum += e.add_v
		e.add_v += 1
		return e.sum

	for i in range( init_n ):
		add()

	return empty.to_attr( e, locals() )
bar = bar_new( 3, 2 )
print( bar.add() )
10

あれ?

そう!

bar_new()呼び出しでinit_nで2回add()実行するところまでは、 foo_new()と同様です。

bar_new()を抜ける直前の

return empty.to_attr( e, locals() )

が問題ありです。

locals() が返す辞書には { 'add_v': 3, 'init_n': 2, ... } としてキー 'add_v' があります。

to_attr() で e.add_v の値が5から3に上書きされて戻ってしまいます。

オブジェクトを生成関数の最後の処理

return empty.to_attr( e, locals() )

kon_ut の色々な場面で多用してきました。

しれっと落とし穴を回避してきてるはずですが、 改めて「わかりにくいな」と実感です。

対策

to_attr(e, dic, **kwds) では、

最後のeへの追加で、 「既に存在するキーについては上書きしない」 という動作にすれば、良さそうです。

empty.pyに add(e, dic={}, **kwds) として、そのような関数を追加して

def bar_new(add_v, init_n=0):
	e = empty.new()
	e.add_v = add_v
	e.sum = 0

	def add():
		e.sum += e.add_v
		e.add_v += 1
		return e.sum

	for i in range( init_n ):
		add()

	return empty.add( e, locals() )
bar = bar_new( 3, 2 )
print( bar.add() )
12

を今後の新たなパターンとして使っていきたいところです。

(サンプルのメソッドとしてもadd()なので、ちょとややこしいですが...)

先日の varsとオブジェクトのアトリビュート の思い違いもあり、 この際 empty.py の実装を見直す機会かと。

empty.py実装の見直し

class Empty:
	pass

def to_attr(e, dic, **kwds):
	kwds.update(dic)
	for (k, v) in kwds.items():
		setattr(e, k, v)
	return e

new = lambda dic={}, **kwds: to_attr( Empty(), dic, **kwds )

new_kwds = lambda **kwds: new(kwds)
	:

この冒頭のあたりが主要部分で、ほぼ全てです。

to_attr()のdicと**kwdsの関係を確かめてみると、

kwds.update( dic )

なので、kwdsをベースに、dicで上書きした辞書になります。

dicとkwdsでは、dicの方が強い仕様です。

逆に言うと、dicをベースに、kwdsからdicに含まれないキーの内容だけの追加と言えます。

そして、引数として与えたdic自体の内容は変化しません。

その「dicと**kwdsとの結果」で、eのアトリビュートを上書きしてます。

次のような関数を用意したら、整理しやすいでしょうか?

def get_dic(base, over_wrt):
	d = base.copy()
	d.update( over_wrt )
	return d

base, over_wrtには辞書を指定します。

baseを下敷きにしてover_wrtの内容で上書きした辞書を返します。

base, over_wrt自体の内容は変化しません。

これを使うとto_attr()は、

def to_attr(e, dic={}, **kwds):
	vars( e ).update( get_dic( kwds, dic ) )
	return e

しかし、しかし、、、

よく考えると、引数の並びからすると、dicをkwdsで上書きする印象があります。

そもそも、これまでのto_attr()の使用場面では、dicとkwdsでキーが重複するような場面は無く。

結構「テキトー」で大丈夫だった感じです。

この際、dic, kwdsは逆の上書き関係に修正した方がすっきりするし、 今なら、互換性の影響も無いかもです。

思い切って仕様を変えてしまいたいところ。

def to_attr(e, dic={}, **kwds):
	vars( e ).update( get_dic( dic, kwds ) )
	return e

new()はそのまま

new = lambda dic={}, **kwds: to_attr( Empty(), dic, **kwds )

new_kwds()は

new_kwds = lambda **kwds: new(kwds)

これは廃止したいところですが、互換性のため

new_kwds = new

とでもして残しておきます。

そして今回追加したいadd()

新規追加なので、dicとkwdsの関係は、順番通りkwdsで上書きタイプに。

そしてto_attr()とは違って、eの既存の部分はそのまま残します。

def add(e, dic={}, **kwds):
	d = get_dic( get_dic( dic, kwds ), vars( e ) )
	vars( e ).update( d )
	return e

思い切って変更すべきか

変更前

class Empty:
	pass

def to_attr(e, dic, **kwds):
	kwds.update(dic)
	for (k, v) in kwds.items():
		setattr(e, k, v)
	return e

new = lambda dic={}, **kwds: to_attr( Empty(), dic, **kwds )

new_kwds = lambda **kwds: new(kwds)

変更後

class Empty:
	pass

def get_dic(base, over_wrt):
	d = base.copy()
	d.update( over_wrt )
	return d

def to_attr(e, dic={}, **kwds):
	vars( e ).update( get_dic( dic, kwds ) )
	return e

new = lambda dic={}, **kwds: to_attr( Empty(), dic, **kwds )

new_kwds = new

def add(e, dic={}, **kwds):
	d = get_dic( get_dic( dic, kwds ), vars( e ) )
	vars( e ).update( d )
	return e

果たして、これで問題なしか?

ダメならまた戻すかもしれません。

一旦、この仕様、実装に変更してみます。


emptyモジュールのto_attr()の効用

C言語の場合代入演算子は値を返します。

古来からK&Rの記述のごとく

int ch;
while ( ( ch = getchar() ) != EOF )
	putchar( toupper( ch ) );

関数呼び出しの結果を変数に代入しつつ、結果による判定をして、後続の処理で関数の結果の値を参照できます。

例えば

	:
int ret = -1, n = 3;
for ( i = 0; i < n; i++ )
	if ( ( ret = foo() ) >= 0 )
		break;
	sleep( 1 );
if ( ret < 0 )
	return err( ret );
	:

pythonでは変数へのバインドは、文であって式ではなく。

値は返しません。

	:
(ret, n) = ( -1, 3 )
for i in range( n ):
	ret = foo()
	if ret >= 0:
		break
	time.sleep( 1 )
if ret < 0:
	return err( ret )
	:

副作用の問題もなく平和なのですが...

ret = foo()
if ret >= 0:

ここ。ここを何とか

if ( ret = foo() ) >= 0:

的な事にしてみたい。

そこで、 kon_ut の empty.py モジュールのto_attr()関数。

import empty
	:
set = empty.to_attr

e = empty.new( ret=-1, n=3 )
for i in range( e.n ):
	if set( e, ret=foo() ).ret >= 0:
		break
	time.sleep( 1 )
if e.ret < 0:
	return err( e.ret )
	:

さらに繰り返し変数iもアトリビュートに保持すると

set = empty.to_attr

e = empty.new( ret=-1, i=0, n=3 )
while set( e, ret=foo(), i=e.i+1 ).ret < 0 and e.i < e.n:
	time.sleep( 1 )
if e.ret < 0:
	return err( e.ret )
	:

決して視認性が向上したとは言えません。

むしろ、引き換えに色々と大切なものを失っている気がします。


リストに空文字列を追加すると

これは以前に 簡易なYAMLパーサ 2018夏デバッグ修正V16 で遭遇した現象です。

当時の記述
$ python
>>> lst = ['']
>>> lst += ''
>>> lst
['']

>>> lst + ''
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

>>> lst = lst + ''
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

原因解りません。なぜか '+=' だと通ります。


最初にリストに入ってる値は別に関係なくて、 リストに文字列の要素を追加したい場面にて。

本来は

>>> lst = []
>>> lst += ['']
>>> lst
['']

とすべきところで、間違えて

>>> lst = []
>>> lst += ''
>>> lst
[]

としてしまっても、エラーにならずに通ってしまいます。

>>> [] + ''
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

だとエラーになるのに、と。

では、空文字でなければ?

>>> lst = []
>>> lst += [ 'abc' ]
>>> lst
['abc']

のところを、手がすべって

>>> lst = []
>>> lst += 'abc'
>>> lst
['a', 'b', 'c']

なんと!

>>> [] + 'abc'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

ですが

'abc' が ['a', 'b', 'c'] ならば

>>> [] + ['a', 'b', 'c']
['a', 'b', 'c']

つまり変数の値がリストオブジェクトで

変数 += 文字列

ならば、文字列が1文字ごとのリストに変換されてから、 リスト同士の足し算になる?

>>> lst = []
>>> lst += 'abc'
>>> lst
['a', 'b', 'c']

そして '+=' でないとこうなりません。'+' では変換されません。

>>> lst = []
>>> lst + 'abc'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

また '+=' なので、変数でないとダメ。

>>> [] += 'abc'
  File "<stdin>", line 1
SyntaxError: illegal expression for augmented assignment

変数でもなく、'+=' でもなければ当然ダメ。

>>> [] + 'abc'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

これは、手をすべらさないように気をつけないと。

見つけにくいバグ、要注意ですね。


SIGINT(^Cキー)で終了するか

threading.Event()のオブジェクトevに対して、ev.wait()で待ってるとき。

^Cキーで終了せず。ショック。

time.sleep()中や、read()ブロック中と同じように、 ^Cキーで終了できるものと思ってたのですが...

でもでも、Macなpython3の環境で試してみると、確かに^Cで止まります ???

ひょっとして止まらないのはpython2だけ?

この際、色々試してみました。

とりあえず、何でもありのプログラムを作成。

bar.py
#!/usr/bin/env python

import sys
import time
import threading
import subprocess

try:
	import queue
except ImportError:
	import Queue as queue

def run():
	s = ' '.join( sys.argv[ 1 : ] )
	print( s )
	exec( s )

if __name__ == "__main__":
	run()
# EOF

まずはtime.sleep()から

$ ./bar.py "time.sleep( 60 )"
time.sleep( 60 )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
KeyboardInterrupt
$

python2, python3 ともに停止。

続いてstdinのリード待ち

$ ./bar.py "sys.stdin.read( 1 )"
sys.stdin.read( 1 )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
KeyboardInterrupt
$

python2, python3 ともに停止。

ショックを受けたイベント待ち

$ ./bar.py "threading.Event().wait()"
threading.Event().wait()
^C^C^C^Z
[1]+  Stopped                 ./bar.py "threading.Event().wait()"
$ kill %1
$
[1]+  Terminated              ./bar.py "threading.Event().wait()"
$

python2 止まりません。

^Zでsuspendしてからkill。

$ ./bar.py "threading.Event().wait()"
threading.Event().wait()
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 551, in wait
    signaled = self._cond.wait(timeout)
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 295, in wait
    waiter.acquire()
KeyboardInterrupt
$

python3 停止。

Queueリード待ちでは

$ ./bar.py "queue.Queue().get()"
queue.Queue().get()
^C^C^Z
[1]+  Stopped                 ./bar.py "queue.Queue().get()"
$ kill %1
$
[1]+  Terminated              ./bar.py "queue.Queue().get()"
$

python2 止まりません。

$ ./bar.py "queue.Queue().get()"
queue.Queue().get()
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/queue.py", line 164, in get
    self.not_empty.wait()
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 295, in wait
    waiter.acquire()
KeyboardInterrupt
$

python3 停止。

python2 はthread系は止まらないっぽい?

LockとCondition

$ ./bar.py "l = threading.Lock(); l.acquire(); l.acquire()"
l = threading.Lock(); l.acquire(); l.acquire()
^C^C^Z
[1]+  Stopped                 ./bar.py "l = threading.Lock(); l.acquire(); l.acquire()"
$ kill %1

[1]+  Stopped                 ./bar.py "l = threading.Lock(); l.acquire(); l.acquire()"
$
[1]+  Terminated              ./bar.py "l = threading.Lock(); l.acquire(); l.acquire()"
$
$ ./bar.py "c = threading.Condition(); c.acquire(); c.wait()"
c = threading.Condition(); c.acquire(); c.wait()
^C^C^Z
[1]+  Stopped                 ./bar.py "c = threading.Condition(); c.acquire(); c.wait()"
$ kill %1
$
[1]+  Terminated              ./bar.py "c = threading.Condition(); c.acquire(); c.wait()"
$

python2 止まりません。

$ ./bar.py "l = threading.Lock(); l.acquire(); l.acquire()"
l = threading.Lock(); l.acquire(); l.acquire()
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
KeyboardInterrupt
$
$ ./bar.py "c = threading.Condition(); c.acquire(); c.wait()"
c = threading.Condition(); c.acquire(); c.wait()
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 295, in wait
    waiter.acquire()
KeyboardInterrupt
$

python3 停止。

やはりthread系。

そして、python3 だと全部問題なし?

プロセス系は?

$ ./bar.py "subprocess.call( 'sleep 60', shell=True )"
subprocess.call( 'sleep 60', shell=True )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
  File "/usr/lib/python2.7/subprocess.py", line 523, in call
    return Popen(*popenargs, **kwargs).wait()
  File "/usr/lib/python2.7/subprocess.py", line 1392, in wait
    pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
  File "/usr/lib/python2.7/subprocess.py", line 476, in _eintr_retry_call
    return func(*args)
KeyboardInterrupt
$
$ ./bar.py "subprocess.check_output( 'sleep 60', shell=True )"
subprocess.check_output( 'sleep 60', shell=True )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
  File "/usr/lib/python2.7/subprocess.py", line 568, in check_output
    output, unused_err = process.communicate()
  File "/usr/lib/python2.7/subprocess.py", line 792, in communicate
    stdout = _eintr_retry_call(self.stdout.read)
  File "/usr/lib/python2.7/subprocess.py", line 476, in _eintr_retry_call
    return func(*args)
KeyboardInterrupt
$

python2, python3 止まります。

Popen()でwait()では?

$ ./bar.py "proc = subprocess.Popen( 'sleep 60', shell=True ); proc.wait()"                                                                      proc = subprocess.Popen( 'sleep 60', shell=True ); proc.wait()
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
  File "/usr/lib/python2.7/subprocess.py", line 1392, in wait
    pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
  File "/usr/lib/python2.7/subprocess.py", line 476, in _eintr_retry_call
    return func(*args)
KeyboardInterrupt
$

python2, python3 止まります。

threadそのもののjoin()待ち

$ ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); th.join()"
th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); th.join()
^C^C^Z
[1]+  Stopped                 ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); th.join()"
$ kill %1

[1]+  Stopped                 ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); th.join()"
$
[1]+  Terminated              ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); th.join()"
$

python2 止まりません。

例えばmain threadでjoin()せずに、time.sleep()してみたら?

$ ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); time.sleep( 60 )"
th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); time.sleep( 60 )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
KeyboardInterrupt

(この間 ^C 連打)

^Z
[1]+  Stopped                 ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); time.sleep( 60 )"
$ kill %1

[1]+  Stopped                 ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); time.sleep( 60 )"
$
[1]+  Terminated              ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.start(); time.sleep( 60 )"
$

どうやら、main threadのsleep()からは抜けてきたものの、

sub thread側が粘ってるので終了せずな感じです。

そう! threadのdaemon属性がありました。

$ ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); time.sleep( 60 )"
th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); time.sleep( 60 )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
KeyboardInterrupt
$

daemon = True にしておくと、main threadが終了すると、sub threadも終了して、結果、終了できてます。

いくらdaemon = Trueにしていても、肝心のmain threadでjoin()待ちしてると

$ ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); th.join()"
th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); th.join()
^C^C^C
^Z
[1]+  Stopped                 ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); th.join()"
$ kill %1

[1]+  Stopped                 ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); th.join()"
$
[1]+  Terminated              ./bar.py "th = threading.Thread( target=( lambda : time.sleep( 60 ) ) ); th.daemon = True; th.start(); th.join()"
$

main threadが終了せず。結果止まりません。

逆に、main threadさえ終了できれば、sub threadでイベント待ちしていようとも...

$ ./bar.py "th = threading.Thread( target=( lambda : threading.Event().wait() ) ); th.daemon = True; th.start(); time.sleep( 60 )"
th = threading.Thread( target=( lambda : threading.Event().wait() ) ); th.daemon = True; th.start(); time.sleep( 60 )
^CTraceback (most recent call last):
  File "./bar.py", line 19, in <module>
    run()
  File "./bar.py", line 16, in run
    exec( s )
  File "<string>", line 1, in <module>
KeyboardInterrupt
$

終了できてます。

python3 で同様に試すと、main thread は th.join() でも time.sleep() でも止まります。

daemon = True な sub thread も、main thread が止まると、止まります。

daemon = True でない sub thread は、main thread が止まっても、止まりませんでした。

まとめ

SIGINT(^Cキー)で止まるか?

package method python2 python3
time sleep
sys.stdin read
subprocess call
check_output
Popen
queue Queue.get ×
threading Event.wait ×
Lock.acquire ×
Condition.wait ×
Thread.join ×
Thread daemon=True
Thread daemon=False × ×


forの動作について

例えば

for.py

#!/usr/bin/env python

def run():
	n = 8
	i = 0
	while i < n:
		print( i )
		i += 1

if __name__ == "__main__":
	run()
# EOF
$ ./for.py
0
1
2
3
4
5
6
7
$

なループ処理は

def run():
	n = 8
	for i in range( n ):
		print( i )

としてfor文にして

$ ./for.py
0
1
2
3
4
5
6
7
$

同様に動作します。

listにして保持しても

def run():
	n = 8
	lst = list( range( n ) )
	for i in lst:
		print( i )
$ ./for.py
0
1
2
3
4
5
6
7
$

同様に動作。

ここまでは、あたり前です。

途中でちょいと list を変更してみたら?

def run():
	n = 8
	lst = list( range( n ) )
	for i in lst:
		print( i )
		lst.pop()

lst.pop()でlistの末尾の要素から削除していってみると

$ ./for.py
0
1
2
3
$

前から表示しつつ、後ろから削除していくので、 listの最初の状態からすると、中ほどで終了。

という事はfor文の判定処理では、 毎回 in の後の記述箇所を参照してるはず?

例えばこれならば?

def run():
	n = 8
	lst = list( range( n ) )
	for i in lst[ : ]:
		print( i )
		lst.pop()

for文のinの後には lst[ : ] として、 lst のコピーを渡してみます。

$ ./for.py
0
1
2
3
4
5
6
7
$

ふむー。

inの後を毎回参照していたとしたら、 毎回「その時点」のlstの内容のコピーが作られて、それが使われているかというと...

動作結果からして、そうでは無いです。

考察するに、、、

for文実行の最初で、inの後の「オブジェクト」を「取り込み」ます。

繰り返し処理では、取り込んだオブジェクトの先頭から順に要素を返していきます。

「取り込み」は最初の1回だけですが、 取り込まれたオブジェクトと同じオブジェクトを変化させると、 for文の中の処理に影響します。

例えば、初回の取り込みでコピーしたオブジェクトを渡しておくと、 for文の繰り返し処理にはコピーした側のオブジェクトが使われるので、 オリジナルのオブジェクトを変化させても、影響はありません。

pythonのユーティリティ・プログラム 2020冬

#empty.py

を使って、ちょっとforの真似をしてみます。

#!/usr/bin/env python

import empty

def for_fake_new(lst):
	e = empty.new()
	e.i = 0
	e.v = None

	def next():
		ret = e.i < len( lst )
		if ret:
			e.v = lst[ e.i ]
			e.i += 1
		return ret

	return empty.add( e, locals() )

def run():
	n = 8
	for_fake = for_fake_new( range( n ) )
	while for_fake.next():
		print( for_fake.v )


if __name__ == "__main__":
	run()
# EOF
$ ./for.py
0
1
2
3
4
5
6
7
$

listに保持してみて

def run():
	n = 8
	lst = list( range( n ) )
	for_fake = for_fake_new( lst )
	while for_fake.next():
		print( for_fake.v )
$ ./for.py
0
1
2
3
4
5
6
7
$

listを末尾から削除していくと

def run():
	n = 8
	lst = list( range( n ) )
	for_fake = for_fake_new( lst )
	while for_fake.next():
		print( for_fake.v )
		lst.pop()
$ ./for.py
0
1
2
3
$

最初にlistのコピーを渡しておくと

def run():
	n = 8
	lst = list( range( n ) )
	for_fake = for_fake_new( lst[ : ] )
	while for_fake.next():
		print( for_fake.v )
		lst.pop()
$ ./for.py
0
1
2
3
4
5
6
7
$

考察通り、同様に動作します。


map()のままでも良い場合

結果一覧

zipの引数
関数の引数への展開
アンパック
for文
mapの引数
filterの引数
スライスによる参照 ×
numpyのarrayの引数 ×
len ×
boolの判定 ×
sumの引数
allとanyの引数
文字列のjoin
dict生成

まだまだありそうですが、とりあえず。

ぼちぼち追加していきます。

概略

python2ではmap()関数はlistを返していました。

python3ではmap()関数はmapオブジェクトを返すように変わりました。

map( func, lst )

としていた箇所を全て

list( map( func, lst ) )

に書き換えるのであれば問題無いハズです。

ですが、

map( func, lst )

のままでも、問題なく動作する場合もあります。

今だに「この場合はどうだったかな?」と、 pythonインタプリタを起動して手を動かして、 簡単に確認する事しばしば。

なので、自分でよく使う場合の記録を取ってまとめておきます。

zipの引数

zip()自体がlistを返さなくなったので、確認がややこしい、、、

>>> zip( [ 1, 2, 3 ], [ 4, 5, 6 ] )
<zip object at 0x10a0866c8>

list()で囲えばOK

>>> list( zip( [ 1, 2, 3 ], [ 4, 5, 6 ] ) )
[(1, 4), (2, 5), (3, 6)]

引数の値をそのまま返す関数 f を用意しておきます。

>>> f = lambda i: i

zipの引数にmap()結果を渡す場合は

>>> list( zip( map( f, [ 1, 2, 3 ] ), map( f, [ 4, 5, 6] ) ) )
[(1, 4), (2, 5), (3, 6)]

OKです。

あと、zip()の引数そのものでは無いですが、よく使うパターンなので...

map()結果を展開してzipに与えるパターン

>>> list( zip( *map( f, [(1, 4), (2, 5), (3, 6)] ) ) )
[(1, 2, 3), (4, 5, 6)]

OKです。

関数の引数への展開

先のzipの例で自明ですが、一応確認を。

>>> f_rev = lambda *lst: [ lst[ 2 ], lst[ 1 ], lst[ 0 ] ]
>>> f_rev( 0, 1, 2 )
[2, 1, 0]
>>> f_rev( *map( f, [ 0, 1, 2 ] ) )
[2, 1, 0]

OKです。

アンパック

>>> (a, b) = [ 1, 2 ]
>>> a
1
>>> b
2

とかのやつです。

>>> (a, b) = map( f, [ 1, 2 ] )
>>> a
1
>>> b
2

OKです。

for文

>>> for i in map( f, range( 3 ) ):
...   print( i )
...
0
1
2

OKです。

mapの引数

>>> list( map( f, map( f, [ 1, 2, 3 ] ) ) )
[1, 2, 3]

OKです。

filterの引数

>>> list( filter( lambda v: v % 2 == 0, map( f, range( 10 ) ) ) )
[0, 2, 4, 6, 8]

OKです。

スライスによる参照

>>> [ 0, 1, 2 ][ 1 ]
1
>>> [ 0, 1, 2 ][ :2 ]
[0, 1]

なところですが

>>> map( f, [ 0, 1, 2 ]) [ 1 ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'map' object is not subscriptable
>>> map( f, [ 0, 1, 2 ]) [ :2 ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'map' object is not subscriptable

これがダメです。

numpyのarrayの引数

>>> import numpy as np

>>> np.array( [ 0, 1, 2 ] )
array([0, 1, 2])

なところを

>>> np.array( map( f, [ 0, 1, 2 ] ) )
array(<map object at 0x10a085978>, dtype=object)

エラーは出てませんが、ダメです。

意味が変わります。意図した結果じゃないです。

len

>>> len( [ 1, 2, 3 ] )
3
>>> len( map( f, [ 1, 2, 3 ] ) )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'map' has no len()

ダメです。

boolの判定

>>> 'T' if [] else 'F'
'F'
>>> 'T' if [ 1, 2, 3 ] else 'F'
'T'
>>> 'T' if not [] else 'F'
'T'
>>> 'T' if not [ 1, 2, 3 ] else 'F'
'F'
>>> bool( [] )
False
>>> bool( [ 1, 2, 3 ] )
True

なやつです。

>>> 'T' if map( f, [] ) else 'F'
'T'
>>> 'T' if map( f, [ 1, 2, 3 ] ) else 'F'
'T'
>>> bool( map( f, [] ) )
True
>>> bool( map( f, [ 1, 2, 3 ] ) )
True

常にmapオブジェクトが返り、Noneでは無いので、Trueばかり。

ダメです。

sumの引数

>>> f = lambda i: i
>>> sum( map( f, [ 1, 2, 3 ] ) )
6
>>> sum( map( f, [ [ 1, 2 ], [], [ 4, 5 ] ] ), [] )
[1, 2, 4, 5]

OKです。

allとanyの引数

>>> f = lambda i: i
>>> all( map( f, [ True, True, True ] ) )
True
>>> all( map( f, [ True, True, False ] ) )
False
>>> any( map( f, [ True, True, False ] ) )
True
>>> any( map( f, [ False, False, False ] ) )
False

OKです。

文字列のjoin

>>> f = lambda i: i
>>> '/'.join( map( f, [ 'foo', 'bar', 'hoge' ] ) )
'foo/bar/hoge'

OKです。

dict生成

>>> f = lambda i: i
>>> dict( map( f, [ ( 'foo', 1 ), ( 'bar', 2 ) ] ) )
{'foo': 1, 'bar': 2}

OKです。


importしたファイルに定義されているグローバル変数の値

例えば

foo.py
v = 123

このファイルfoo.pyを別のファイルでimportして参照します。

bar.py
import foo

pythonインタプリタを起動します。

$ python

foo.pyをimportして

>>> import foo

foo.pyのvの値は当然

>>> foo.v
123

です。

変更も

>>> foo.v = 0
>>> foo.v
0

可能です。

この状態からbar.py経由でfoo.pyをimportしてみると、どうか?

>>> import bar
>>> bar.foo.v
0

bar.pyからimportされているfoo.pyは、 先にインタプリタから直接importしたものと同じものが見えてます。

>>> bar.foo.v = 999
>>> foo.v
999

確かに同じvが見えてます。

例えば、このようなhoge.pyを用意すると?

hoge.py
import foo
foo.v = 777

>>> foo.v
999
>>> import hoge
>>> foo.v
777

なるほど。importした瞬間に値が書き換わりますね。

>>> foo.v = 'hellow'
>>> bar.foo.v
'hellow'
>>> hoge.foo.v
'hellow'

そして、どの経路からアクセスしても、同じものが共有されていますね。


mapとsum

map, filter, reduceについて

python3ではreduceがimportが必要になって、ついつい敬遠しがちになってしまいました。

mapはリスト各要素に変換をかけて、結果のリストの要素数は変わらず。 (リストと言ってしまってはアレですが...)

filterは各要素を「ふるい」にかけて、要素の値は変わらずに、要素数が減ります。

要素に変換をかけつつ、要素数を減らしたり、あるいは逆に要素数を増やしたい状況が、ちょいちょいあります。

例えば、ファイル丸ごとリードした文字列を、まず改行で区切って行のリストに。

s = f.read()
lst = s.strip().split( '\n' )

その行のリストの各行について、 空白で区切って単語のリストにしたいときなど。

>>> lst = [ 'foo', 'bar hoge', '', 'fuga guha', '' ]
>>> f = lambda s: s.strip().split( ' ' )

を用意して

>>> list( map( f, lst ) )
[['foo'], ['bar', 'hoge'], [''], ['fuga', 'guha'], ['']]

変換関数 f では、 要素の値である「1行分の文字列」に変換をかけて、単語の「リスト」 を返すので、まぁそうなります。

変換結果が、「必ずリストを返す」というお決まりならば。

変換結果が1つなら、要素1つのリスト。

2つなら、要素2つのリスト。

削除したいなら空のリスト。

ならば、それらのリストを sum( リスト, [] ) で合計すると、リストの平滑化。

という事で

>>> sum( map( f, lst ), [] )
['foo', 'bar', 'hoge', '', 'fuga', 'guha', '']

もうひとこえ

>>> f = lambda s: list( filter( lambda w: w, s.strip().split( ' ' ) ) )

ならば

>>> f( 'foo' )
['foo']
>>> f( 'abc def' )
['abc', 'def']
>>> f( ' ' )
[]

となって、変換関数は必ずリストを返します。

結果の要素数が0ならば、削除。

>>> lst
['foo', 'bar hoge', '', 'fuga guha', '']

>>> sum( map( f, lst ), [] )
['foo', 'bar', 'hoge', 'fuga', 'guha']

となりましょう。

>>> map_sum = lambda f, lst: sum( map( f, lst ), [] )

としておいて

>>> map_sum( f, lst )
['foo', 'bar', 'hoge', 'fuga', 'guha']

pythonのユーティリティ・プログラム 2020冬 のどこかに入れておきたい気もしますが、 なんか単純すぎて、毎回その場で定義しても良いかなと。


変数や内部関数の定義順の違い

まず変数について。

$ python
>>> def foo():
...         a = 1
...         b = 2
...         c = a + b
...         print( locals() )
...
>>> foo()
{'a': 1, 'c': 3, 'b': 2}

なところを、定義前に参照すると

>>> def foo():
...         a = 1
...         c = a + b
...         b = 2
...         print( locals() )
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in foo
UnboundLocalError: local variable 'b' referenced before assignment

当然、怒られます。

内部関数でも

>>> def foo():
...         a = 1
...         b = 2
...         c = a + b
...         def bar():
...                 d = b + c
...         bar()
...
>>> foo()
>>>

なところを、定義前に呼び出すと

>>> def foo():
...         a = 1
...         b = 2
...         c = a + b
...         bar()
...         def bar():
...                 d = b + c
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in foo
UnboundLocalError: local variable 'bar' referenced before assignment

怒られます。

このような事は?

>>> def foo():
...         def bar():
...                 d = b + c
...         a = 1
...         b = 2
...         c = a + b
...         bar()
...
>>> foo()
>>>

これは通ります。

bar()を定義した時点では、bもcも定義されてません。

ですが、bar()を呼び出したときには、b, c は定義されています。

なので

>>> def foo():
...         def bar():
...                 d = b + c
...         bar()
...         a = 1
...         b = 2
...         c = a + b
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in foo
  File "<stdin>", line 3, in bar
NameError: free variable 'b' referenced before assignment in enclosing scope

これは、怒られます。

bar()を呼び出した時点で、b, cが定義されていません。

内部関数から内部関数を呼び出す場合でも

>>> def foo():
...         def bar():
...                 hoge()
...         bar()
...         def hoge():
...                 d = b + c
...         a = 1
...         b = 2
...         c = a + b
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in foo
  File "<stdin>", line 3, in bar
NameError: free variable 'hoge' referenced before assignment in enclosing scope

bar()を呼び出したときに、hoge()は定義されておらず、怒られます。

>>> def foo():
...         def bar():
...                 hoge()
...         def hoge():
...                 d = b + c
...         bar()
...         a = 1
...         b = 2
...         c = a + b
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in foo
  File "<stdin>", line 3, in bar
  File "<stdin>", line 5, in hoge
NameError: free variable 'b' referenced before assignment in enclosing scope

bar()呼び出したときに、hoge()は定義されているのですが、 hoge()で参照しているb, cが定義されておらず、そこで怒られます。

>>> def foo():
...         def bar():
...                 hoge()
...         def hoge():
...                 d = b + c
...         a = 1
...         b = 2
...         c = a + b
...         bar()
...
>>> foo()
>>>

これだと、無事に通ります。

例えば、bar()の実行をタイマーで1秒後に。

a の定義を2秒後に。

>>> import threading
>>> import time

>>> def foo():
...         def bar():
...                 b = a
...         threading.Timer( 1, bar ).start()
...         time.sleep( 2 )
...         a = 1
...
>>> foo()
Exception in thread Thread-2:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 1073, in run
    self.function(*self.args, **self.kwargs)
  File "<stdin>", line 3, in bar
NameError: free variable 'a' referenced before assignment in enclosing scope

aの定義が間に合わずに怒られます。

ですが

>>> def foo():
...         def bar():
...                 b = a
...         threading.Timer( 1, bar ).start()
...         time.sleep( 0.5 )
...         a = 1
...
>>> foo()
>>>

aの定義を0.5秒後にすると、通ります。

bar()の1秒後の実行は、aの定義後になるのでOKです。


bytesのスライス

実に、3年弱ぶりの追記です。

byesと文字列は似たような物だろうと永年思っていましたが、落とし穴にはまりました。

結論

文字列から1文字取り出したら、それは文字列

バイト列 (bytes) から1バイト取り出したら、それは整数 (int) !!!

ただし、バイト列 (bytes) から「1バイト列」を取り出したら、それはバイト列 (bytes)

実行例

文字列

>>> s = "あいう"

>>> type( s )
<class 'str'>

>>> s[ 1 ]
'い'
>>> type( s[ 1 ] )
<class 'str'>

問題なく、今まで思っていた通りです。

バイト列 (bytes)

>>> bt = s.encode()

>>> bt
b'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'

>>> type( bt )
<class 'bytes'>

>>> bt[ 1 ]
129

>>> type( bt[ 1 ] )
<class 'int'>

int です!

ただし、

>>> bt[ 1 : 2 ]
b'\x81'

>>> type( bt[ 1 : 2 ] )
<class 'bytes'>

>>> 0x81
129

こう切り出すと、1バイトでもバイト列 (bytes)

ハマった案件

例えば、文字列のお尻から1文字ずつバッファに移動さして処理するケース

s = "foo bar"

buf = ""
while s != "foo":
	buf = s[ -1 ] + buf
	s = s[ : -1 ]
	:

s が "foo" で、buf に " bar" が貯まります。

ここで、utf-8でバイト列 (bytes) にしても同じだろう?

s = "foo bar"
bt = s.encode()

buf = b""
while bt != "foo".encode():
	buf = bt[ -1 ] + buf
	bt = bt[ : -1 ]
	:

エラーでます!

>>> s = "foo bar"
>>> bt = s.encode()

>>> buf = b""
>>> while bt != "foo".encode():
...   buf = bt[ -1 ] + buf
...   bt = bt[ : -1 ]
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'bytes'
>>>
buf = bt[ -1 : ] + buf

にすれば OK

雑感

まぁ、C言語で

char *s = "hoge";
char c = s[ 0 ];
int ch = s[ 1 ];

的な、文字列と文字の感覚からしたら、あまり違和感ない気がします。

むしろ、pythonの文字列の扱いの方こそが特殊。

使用したバージョン

$ python3
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>