modalsoul’s blog

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

国際フレンドシップ・デーで、Intellij IDEAのライセンスを50%offで買った

タイトルの通りです

前回の割引のときに買いそびれて後悔してたんで、心残りをひとつ成仏させてやれた、よかったよかった

国際フレンドシップ・デーや、なんで割引なのか?について詳しくは↓を blog.jetbrains.com

↓の紹介リンクから買うと-¥500になります。よろしかったらどうぞ secure.samuraism.com

新規サブスクリプションだけじゃなく、継続にも適用されるらしいですね

Seleniumスクリプトをdocker上のHeadless Chromeで動かす

Seleniumを使った時間のかかるPythonスクリプトがあり、こいつを動かすとフォーカス取られて仕事にならないので、docker化し、ヘッドレス環境で実行するようにした

Dockerfile

ChromeDriverとSeleniumをインストール

FROM python:3.6-alpine3.7
ENV APP_ROOT=${APP_ROOT:-/opt/app}

# update apk repo
RUN echo "http://dl-4.alpinelinux.org/alpine/v3.7/main" >> /etc/apk/repositories && \
    echo "http://dl-4.alpinelinux.org/alpine/v3.7/community" >> /etc/apk/repositories

# install chromedriver
RUN apk update
RUN apk add chromium chromium-chromedriver

# install selenium
RUN pip install selenium

WORKDIR $APP_ROOT/scripting

Makefile

DOCKER=docker

scripting:
    $(DOCKER) build -t scripting -f Dockerfile .

イメージをビルドする

make scripting

docker-compose.yml

version: '2'
services:
  script:
    image: scripting
    volumes:
      - .:/opt/app/scripting:rw

script.py

ヘッドレス環境で実行するテスト用スクリプト

from selenium import webdriver

URL = 'https://www.google.co.jp/'

def main():  
  options = webdriver.ChromeOptions()
  options.add_argument('--headless')
  options.add_argument('--no-sandbox')
  
  driver = webdriver.Chrome(chrome_options=options)
  
  driver.get(URL)
  print(driver.title)
  
  driver.close()
  driver.quit()

main()

--headlessオプションでヘッドレスモード

--no-sandboxオプションについては↓を参照 github.com

実行

> docker-compose run scripting python3 script.py
Google

動いた

日本語

日本語を表示するためにフォントを入れます

FROM python:3.6-alpine3.7
ENV APP_ROOT=${APP_ROOT:-/opt/app}

# update apk repo
RUN echo "http://dl-4.alpinelinux.org/alpine/v3.7/main" >> /etc/apk/repositories && \
    echo "http://dl-4.alpinelinux.org/alpine/v3.7/community" >> /etc/apk/repositories

# install chromedriver
RUN apk update
RUN apk add chromium chromium-chromedriver
RUN apk add ttf-freefont

# install selenium
RUN pip install selenium

# install font
RUN mkdir /noto

ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto

WORKDIR /noto

RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    fc-cache -fv

WORKDIR $APP_ROOT/scraping

任意の桁数のゾロ目を生成する

n = 1のとき、f(n) = 1
n = 2のとき、f(n) = 11
n = 3のとき、f(n) = 111
n = kのとき、f(n) = 11...11 (k桁の1のゾロ目)

となるf(n)の実装を考える

数式で表すと { \displaystyle
f_n = \sum_{i=0}^{n} 10^{i}
}


ぱっと思いついた再帰を使うパターン

def fn(n, acc=0):
  return acc if(n == 0) else fn(n-1, acc*10+1)

部分和{ \displaystyle10^{i}} の総和だから、

再帰を使わないパターン

sum(10**k for k in range(n))

任意の値xをかければ、xの任意の桁数のゾロ目が生成できる

Pythonicなリストのfilter処理

Pythonicにリストをfilterするにはどうするか?といい話題がでたので、ちょっと考えてみた


話のもとになったslackの投稿(とその擬似コード

  • パターンA
names = [x for x in names if x is CONDITION]

↑だとxに何が入ってくるのかがわからない

  • パターンB
names = [name for names in names if name is CONDITION]

↑長い

  • パターンC
names = list(filter(lambda x: x is CONDITION, names))

lambda x:の部分はフィルタの関数が定義されているだけなので、xが何かわからなくてもいい?

どれがPythonicで良いコードなのか?


自分個人の見解としてはfilterを使いたい

理由としては、filterのほうがフィルターしていることが明示的だから。

フィルターしていることがわかると、引数にとったリストの要素の型と同じ型の要素を持つリストが帰ってくることが読み取れる

擬似的にScalaで型を表記すると、

val list:List[A]なら、filter(f, list)List[A]であるということ


それと、filterに渡すフィルタの関数の引数が何であるかは気にしなくて良いと思う

フィルターであれば、何が入ってくるかより、何になればフィルターの条件を満たすのかがわかればよくて、極論変数名もいらないと思う

Scalaの場合、関数のすべての引数が1回のみ使われる場合、引数宣言を省略して_で記述できる

ex)

names.filter( _ == HOGE)

なので、Pythonでもこんな風に書きたいくらい

names = list(filter(lambda _: _ is HOGE))

実際これでも動く


最終的にSlack上の議論では、内包表記がよいということになった

理由としては、可読性が良いから

可読性がよいと判断する理由としては、内包表記のほうがより一般的であるため

複数人でメンテナンスするコードであれば、尚更大事なポイント


それと、Effective Pythonでも内包表記を薦めているし

qiita.com

内包表記のほうがfilterの10倍くらい速いらしい

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は非正格なことが確認できた