takaha.siの技術メモ

勉強したことをお伝えします。ちょっとでも誰かの役に立てればいいな…

pythonのデフォルト引数の挙動は可怪しいという話

Pythonをすごいやりたい!ってわけではないんですが、DiscordのBot作るのにPython便利そうなので使ってます。

で、プログラミング言語にはデフォルト引数っていう機能がありますよね?Pythonにもあるんですが、Pythonのデフォルト引数の仕様は普段私が使ってる言語たち(Ruby, CL, Scala etc)と仕様がちょっと違うのでハマりましたよという話。

import time
import datetime
class Foo:
    def bar(self, now = datetime.datetime.now()):
        print(f"{now}")
hoge = Foo()
while True:
    hoge.bar()
    time.sleep(1)

ここでbarっていうFooクラスのインスタンスメソッドがあったとして、普通barが呼ばれるたびにnowは更新されると思いませんか?。ところがpythonってそうならないんですよね。。。classがnewされたときのnowが固定で代入されてずーっと呼び出されます。

私このPythonの意味不明仕様をしらなかったもんで、これに1時間ぐらいハマりました。ユニットテストでは通るのに、実際にコードを動かすと思ったように動かない。

他の言語ではどうなんですか?っていう話ですが

例えばRuby

require 'date'
class Foo
  def bar(now = Time.now)
    puts now
  end
end
hoge = Foo.new()
while true
  hoge.bar()
  sleep(1)
end

Rubyの結果

2020-12-29 15:47:16 +0900
2020-12-29 15:47:17 +0900
2020-12-29 15:47:18 +0900
2020-12-29 15:47:19 +0900
2020-12-29 15:47:20 +0900
2020-12-29 15:47:21 +0900
2020-12-29 15:47:22 +0900
2020-12-29 15:47:23 +0900
^CTraceback (most recent call last):
        1: from test.rb:14:in `<main>'
test.rb:14:in `sleep': Interrupt

普通はこうですよね!メソッド呼び出しのたびにデフォルト引数内部は更新されますよ!変わりますってば!

Scalaの場合

class Hoge {
  def _now() {
  }
}
class Foo {
  def bar(now: Hoge = new Hoge()) {
    println(now)
  }
}
val hoge = new Foo()
while (true) {
  Thread.sleep(1000)
  hoge.bar()
}
Main$$anon$1$Hoge@16f65612
Main$$anon$1$Hoge@311d617d
Main$$anon$1$Hoge@7c53a9eb
Main$$anon$1$Hoge@ed17bee
Main$$anon$1$Hoge@2a33fae0
Main$$anon$1$Hoge@707f7052
Main$$anon$1$Hoge@11028347
Main$$anon$1$Hoge@14899482
Main$$anon$1$Hoge@21588809
Main$$anon$1$Hoge@2aae9190
Main$$anon$1$Hoge@2f333739
Main$$anon$1$Hoge@77468bd9
Main$$anon$1$Hoge@12bb4df8

Scalaの場合もそうです。呼び出しごとに更新されます。当たり前です。

Common Lispの場合

(let ((foo ()))
  (defun bar (&optional (now (random 100)))
    (print now)))

(loop (progn
        (bar)
        (sleep 1)))
14 
69 
6 
48 
60 
73 
8 
31 
43 
4 
6 
71 
14 
81 
78 
14 

いや普通(世の中に出回ってる言語の多数派)こうだと思いますが。。。はて。。。?

Pythonってこの他にも「なぜこうした?」っていう意味不明な仕様が多くて、例えば、mapfilterといった関数もmap objectとかfilter objectとかが返ってきたりしますよね?

いやこういうのって、普通基底クラスなりなんなりにEnumerableとかTraversableみたいなのがあって、ArrayとかListとかはそいつを継承させるものなんじゃないんですか?だからmap objectとかfilter objectとか別途用意するんじゃなくて、mapの結果が直接ArrayとかListを出せるようにするべきなんじゃないんですかね?とかとか