🐍

Python の 罠とか腑に落ちないところとか

に公開

これは何?

Python 熟練者ではない私が、Python を触るとき、これは罠になるなと思った点や、この文法は腑に落ちないなと思った点、これは困るなと思った点を挙げた記事。

事例

スコープ

Python のスコープのルールがちょっと意外。

Python3.13
if True:
    a="if"
print(a) #=> if

for c in ["item"]:
    b="for"
print(b) #=> for
print(c) #=> item

[d:=e for e in ["comp"]]
print(d) #=> comp
print(e) #=> エラー

制御構造はスコープを作らない。

私としては、for c in 略:cfor の外から見えるのはかなり意外。

リスト内包表記の [略 for e in 略]e は見えない。

まあ ruby も同様で

ruby3.4.5
if true
  a=:if
end
p a #=> :if

for c in [:item]
  b=:for
end
p [b,c] #=> [:for, :item]
[:each].each{ |e| d=:each }
p d #=> エラー
p e #=> エラー

となるのだけれど。

obj[:] の挙動

Python3.13
import numpy as np

def test(x):
    y=x[:];y[1]=100;print(x,y)

test([11,22,33]) #=> [11, 22, 33] [11, 100, 33]
test(np.array([11,22,33])) #=> [ 11 100  33] [ 11 100  33]

list の [:] はコピーするけど、numpy の [:] はコピーしない。
違う動作なら違うインターフェイスにしてほしい。

obj+=x

Python3.13
a=b="hoge"
b+="fuga"
print(a,b) #=> hoge hogefuga

a=b=[1,2,3]
b+=[11,22]
print(a,b) #=> [1, 2, 3, 11, 22] [1, 2, 3, 11, 22]

a=b=[1,2,3]
b=b+[11,22]
print(a,b) #=> [1, 2, 3] [1, 2, 3, 11, 22]

+= という演算は、レシーバが mutable だと レシーバを変更する。(標準ライブラリの list なんかはそう)

つまり、a=a+ba+=b の動作はだいぶ違う。

クラス変数の場合

+= の挙動とクラス変数の仕様が合体すると下記のようにわかりにくいことになる

Python3.13
class Base:
    a = ""
    b = []
    @classmethod
    def showAB(cls):
        print( {"a":cls.a, "b":cls.b} )

class Deri(Base):
    @classmethod
    def add(cls):
        cls.a += "x"
        cls.b += [1]
        print( {"a":cls.a, "b":cls.b} )

Deri.add() #=> {'a': 'x', 'b': [1]}
Base.showAB() #=> {'a': '', 'b': [1]}

わかりにくいと思う。

tupleとの組み合わせ

tuple は immutable なので変更できないようにしたいという気持ちはわかるんだけど

Python3.13
a=([1],[2])

try:
    a[0]+=[9]
except TypeError as e:
    print(e) #=> 'tuple' object does not support item assignment

print(a) #=> ([1, 9], [2])

この挙動は困る。

a[0]+=[9]+= が例外を出すなら、+= を行う前の状態で例外を出してほしい。

list に対する += は実質 append なんだから、 a[0].append(9) が例外を出さないのと同様、例外を出さないでほしいということでもある。

※ どうしてこういう結果になるのかはまあまあ理解しているけど、その挙動が好きになれないという話。

式と文

lambda に渡せるのは、式。そして、制御構造は式ではない。
ということで、せっかく lambda があるのに lambda にちょっと複雑なことをやらせようとすると、ああもうここには書けないね、となる。

del

del x[i] が腑に落ちない。

x.delete_at(i) なら、x に「i 番目を delete_at しろ」と伝える、ということで気持ちがわかる。
delete(x, i) でも「x と i を引数に、delete という作業をしろ」なのでわかる。

でも、 del x[i] は腑に落ちない。この文には x[i] という字句が含まれているけど、通常の式にある x[i] に相当する計算を行う感じじゃないところが腑に落ちないポイントなのかなと思っている。

del で何ができるかが、listcollections.dequenumpy.array で異なるところもひっかかる。

値 \ 操作 del a del a[0] del a[:]
a=list(略) ✅️ ✅️ ✅️
a=collections.deque(略) ✅️ ✅️ ❌️
a=np.array(略) ✅️ ❌️ ❌️

パッケージと環境の管理

色々あって、わからない。

  • virtualenv
  • venv
  • pyenv
  • Anaconda (conda)
  • pipenv
  • poetry
  • uv
  • asdf
  • rye

が選択肢なのかな(わかってない)。

「昔はみんな xx を使ってたけど、今は yy か zz がいいみたい」ぐらいに狭まっていればいいんだけど、そうでもないような(わかってない)。

何を選ぶのが正解なのかわからない。最近は poetry を使っている気がするけど、いい考えなのかどうかわからない。

最後に

こうして色々挙げたものの、好きなところもある。

たとえば、ループに else があるところ。多倍長整数が普通に使えるところ、a<b<=c みたいに比較できるところ、なんかは好き。

この記事で挙げたポイントも、他の言語・処理系 同様、歴史的経緯とか、実は xx という理由でこの仕様しかないとか、色々あるんだと思う。

numpy や matplotlib なんかが必要な場面があり、これやるあなら Python だよねとなりがちなので、罠にハマらないようにうまく付き合っていきたいと思う。

Discussion