modalsoul’s blog

これは“失敗”と呼べるかもしれないが、ぼくは“学習体験”と呼びたい

Pythonのnamedtupleを使ってScalaのcase class感覚でvalue objectを作る

複数の値をまとめて扱いたい時ありますよね。value object欲しいなって時

そんなとき、Scalaだとcase classを使いますね

Case Classes | Scala Documentation

case class Human(height:Int, weight:Int)

こんな感じで、1行だけで複数の値をまとめて扱うことができます。

付随して、コンパニオンオブジェクトやメソッド群が追加されるので

applyメソッドが実装され、newを省略できたり

scala> val taro = Human(170, 60)
taro: Human = Human(170,60)

unapplyメソッドが実装され、extractorパターンによるパターンマッチができたり

scala> taro match {
     | case Human(h,w) => w/(h*h/10000.0)
     | }
res42: Double = 20.761245674740483

プロパティはimmutable

scala> taro.height = 100
<console>:12: error: reassignment to val
       taro.height = 100

equalsメソッドが適切に実装され、同じプロパティを持つインスタンス同士を同値と判定できたり

scala> taro == Human(170,60)
res43: Boolean = true

と、いろいろ便利です


で、Pythonで複数の値をまとめていい感じに扱うには?

同僚に教えてもらったnamedtupleが便利そうです

8.3. collections — コンテナデータ型 — Python 3.6.3 ドキュメント

case classと同じく、1行でパパっと定義できて

>>> Human = namedtuple('Human', ('height', 'weight'))

サクッと、インスタンスが作れます

>>> taro = Human(height=170, weight=60)
>>> taro
Human(height=170, weight=60)

__str__メソッドを定義しなくとも、オブジェクトの中身を確認できてますね

オブジェクトの属性へアクセスできるし

>>> taro.height
170

属性はimmutable

>>> taro.height = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

そしてunpacking assignmentで、値を取り出せる

>>> for h,w in [taro]:
...     print(w/(h*h/10000))
...
20.761245674740483

同じ属性を持つインスタンス同士の比較もOK

>>> taro == Human(height=170, weight=60)
True

ぱっと見、クラスを定義してるように見えなくて、ナニコレ?ってなるけど、わかるととても便利なやつ

Pythonのタプルでdestructuring bindingっぽい何か(unpacking assignment)でハマった

SQLArchemyを使ったアプリケーションで↓なコードがあった(詳細は割愛)

(age,) = db_session.query(User.age).filter(User.id == input.id).one()

右辺がUser.ageをラップしたなにかなのはわかるんだけど、左辺がよくわからない


これと似たようなもので、Scalaではdestructuring bindingというのがあって、

scala> case class User(age:Int, name:String)
defined class User

scala> val user = User(38, "Valentino Rossi")
user: User = User(38,Valentino Rossi)

scala> val User(age, _) = user
age: Int = 38

のように、右辺のタプルを展開して、左辺の変数に代入される


これと同じことなんだろうと思ったんだけど、左辺でage以外を握り潰しているっぽく見える書き方がよくわからない

(age,)のカンマで終わってるのが、Scalaでいうval User(age, _)ワイルドカードのような働きをしている?

でも、これだと

>>> user = (38, "Valentino Rossi")
>>> (age,) = user
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 1)

となって、握りつぶせていない

よくよく調べてみると、これはunpacking assignmentと呼ばれるもので、右辺がリストなどのコレクションの場合、左辺にカンマ区切りで変数を並べると、左辺の各変数に対応する順で右辺の値が代入されるようだ

そして、今回のケースで混乱の素になったのが、左辺は単要素タプルだということ

Pythonのリファレンスでタプル型を確認すると、

3. データモデル — Python 3.6.3 ドキュメント

タプル型 (tuple)

タプルの要素は任意の Python オブジェクトです。二つ以上の要素からなるタプルは、個々の要素を表現する式をカンマで区切って構成します。単一の要素からなるタプル (単集合 'singleton') を作るには、要素を表現する式の直後にカンマをつけます (単一の式だけではタプルを形成しません。これは、式をグループ化するのに丸括弧を使えるようにしなければならないからです)。要素の全くない丸括弧の対を作ると空のタプルになります。

とあり、今回のケースではageが単一の要素で、その直後にカンマがつくことでタプルになっている。そして、右辺も単要素のタプルであり、その値がageに代入されているということ

左辺でage以外を握り潰すことなんかしていなく、そもそも握り潰す値がなかったんや!

なので、右辺が2要素からなるタプルの場合、ValueErrorとなった


仕組みは理解できたからいいんだけど、こういうケースがggりずらくてつらくない?おれはつらい

Pythonのand, orやallの挙動が正格に見えたけど、ちゃんと非正格だった話

会社のSlackでの1コマ


Pythonorallで、式の評価を途中でやめてくれない

>>> print('a') or pring('b')
a
b

↑の場合、直感的にはbは出力されないはずだけど、バッチリでてる

Pythonって正格なのか、、?と思うも、

>>> 0 and print('b')
0

こっちだとorは非正格に見える

この差異はprintの返り値がNoneのために起きてる

Noneの真偽値は、False

>>> bool(None)
False

なので、↑のケースでprintの結果にTrueを期待してると、裏切られると言う訳


次にall

printFalseに評価されることを踏まえて、

def a():
  print('a')
  return True

def b():
  print('b')
  return False

def c():
  print('c')
  return True

とすると

result = all([a(), b(), c()])
print(result)

result2 = (a() and b() and c())
print(result2)

a
b
c
False
a
b
False

という結果になる

andは非正格だけど、all正格に見えてしまう


これにもタネがあって、↑のa,b,call実行時ではなく、リスト生成時に出力されているから

>>> [a(), b(), c()]
a
b
c
[True, False, True]

こんな感じ

なので、改めてallを検証すると

class A:
  def __bool__(self):
    print('a')
    return True

a = A()

とすると、

>>> all([True, a, False, a])
a
False

となって、allは非正格なことが確認できた

ブログを移行した

GitHub+Jekyllで運用してたブログをはてなブログに移行した

旧ブログ http://modalsoul.github.io/ は、記事公開するのに

と手順が面倒だった

自作jekyll pluginを使わなければgit commit/pushでOKなんだけど、過去記事の移行が面倒なのでブラウザで完結できるはてなブログにした

今年はちょくちょくUPしようと思う