いものやま。

雑多な知識の寄せ集め

『処理から設定へ』というLTをしてみた。

8/24(水)におなじみラスクさんのLT会で『処理から設定へ』というLTをしてみた。

この「処理を設定に抜き出すことで見通しをよくする」という手法、たぶんみんな無意識にやってるんだけど、ちゃんと名前がつけられていない印象。 本とかでも見たことない気がするんだよなぁ。

ちなみに自分がこの手法をちゃんと知ったのは研修でオセロのプログラムを書いてたとき。 石が置けるかのチェックで、普通は上、右上、右、・・・と走査するコードをそれぞれ書くことになるけど、これをdx, dyという設定として持つことで、コードは1つで済むようになるというもの。 そのときの講師に言われてなるほどとすごく感心したのを覚えている。 この手法は変種オセロを書いたときにも使ってる。

あと、FizzBuzzの書き換えの方法を使うと、未使用変数の話をしたときのコードは次のように書けたりもする:

from typing import Callable, Any

# ルール記述のための部品

AcceptFunc = Callable[[str], bool]
TransFunc = Callable[[str], str]

def is_between(first: str, last: str) -> AcceptFunc:
    return lambda c: first <= c <= last

def always(c) -> bool:
    return True

def add_offset(offset: int) -> TransFunc:
    return lambda c: chr(ord(c) + offset)

def identity(c) -> str:
    return c

def convert_with_rules(c: str, rules: list[dict[str: Any]]) -> str:
    for rule in rules:
        if rule["accept?"](c):
            return rule["convert"](c)

# ルール記述

TRANS_RULES = [
    {"accept?": is_between("a", "m"), "convert": add_offset(13)},
    {"accept?": is_between("A", "M"), "convert": add_offset(13)},
    {"accept?": is_between("n", "z"), "convert": add_offset(-13)},
    {"accept?": is_between("N", "Z"), "convert": add_offset(-13)},
    {"accept?": always, "convert": identity},
]

# 変換メソッド

def transform_letter(c: str) -> str:
    return convert_with_rules(c, TRANS_RULES)

def transform(input_: str) -> str:
    return "".join(map(transform_letter, input_))

部品の部分がラムダ式を返す関数だったりして関数型の考えに慣れてないとちょっと分かりにくいけど、面白いのはルール記述の部分で、変換のルールがそのまま記述された感じになっているのが分かるかと思う。 実際には部品の部分はライブラリとして提供すればいいので隠蔽できるし。

今日はここまで!

asyncioやめてほしいというボヤき。

だいぶ経ってしまったけど、8/4(木)にラクスさんで『asyncioが地獄だった件』というLTをした。

以前のLT(『Pythonでリファクタリングできなかった話』をLTしてみた。 - いものやま。)を反省して、asyncioの詳細には踏み込まず、Requests-HTMLというライブラリが便利だよという話と、そのときにasyncioには気をつける必要があるよという話だけにした。 実際、このRequests-HTML自体はお手軽だし。

そのあと、『みんなのPython勉強会 #84』でもasyncioが取り上げられてたり、非同期処理に対する関心は高いのかなという感じがするけど、正直asyncioはやめてほしいよなぁという感じ。 この勉強会でWebアプリを非同期にする話が出てたけど、想像以上に変更が必要みたいで、かなり目眩がした。。。

「asyncio使うなっていうんなら、代わりにどうしろっていうんだ」っていう声も聞こえてきそうだけど、非同期処理をオブジェクト指向的に実現してるconcurrent.futuresを使うのがよさそう。 こちらの方が言語仕様に影響を与えてないし、使い方も簡単だし、呼び出し側が同期か非同期化を選べる。

concurrent.futuresについてはまたどこかでちゃんと取り上げたい。

それにしても、Pythonはどうしてこうも場当たり的に言語仕様を増やすのかなぁ。 本質が見えていなすぎでしょ。。。

今日はここまで!

未使用変数の話。

昨日、リファクタリングに関するLTをしたことさんの発表に関していろいろ思ったことがあったので、ちょっと書いておきたい。

nikkieさんのスライドはこちら:

発表概要

詳細はスライドを見てもらうとして、ざっくりいうと「リファクタリングする場合、驚くほど小さいステップで進めていった方がいいよ」という話。

その中で自分が「ちょっとこれはなぁ」と思ったのが、上記スライドの発表後記にも付け加えられてる通り「一手目として変数追加だけ」というもの。

nikkieさんのリファクタリング前としているコードは以下:

def transform(input_: str) -> str:
    if not isinstance(input_, str):
        raise TypeError("Expected string parameter")

    result = ""
    for character in input_:
        char_code = ord(character)
        result += transform_letter(char_code)
    return result


def transform_letter(char_code: int) -> str:
    if is_between(char_code, "a", "m") or is_between(char_code, "A", "M"):
        char_code += 13
    elif is_between(char_code, "n", "z") or is_between(char_code, "N", "Z"):
        char_code -= 13
    return chr(char_code)


def is_between(char_code: int, first_letter: str, last_letter: str) -> bool:
    return code_for(first_letter) <= char_code <= code_for(last_letter)


def code_for(letter: str) -> int:
    return ord(letter)

ここで「is_between()では文字コードで大小比較してるけど、文字で直接大小比較できるよね」というのが最初のリファクタリングの発想で、その第一歩とされたのが次の変数追加:

-        result += transform_letter(char_code)
+        result += transform_letter(character, char_code)

-def transform_letter(char_code: int) -> str:
+def transform_letter(letter: str, char_code: int) -> str:

でも、これって未使用の変数を追加することになってて、それってよくないよって話をしたい。 やるなら中途半端な状態を作っちゃダメ。

未使用変数の問題

意図が分からない

未使用変数があると、それを読んだ人は「あれ? この変数使ってないけど、いいのかな?」って引っかかりを覚える。 そういう疑問を浮かべてしまうコードは良くないコードで、いわゆる「コードの臭い」そのもの。

そういった「コードの臭い」を取り除くのがリファクタリングなのに、一時的とはいえ、それが埋め込まれたコミットが生じてしまうのは問題。

使うつもりで変数を増やしたのに間違って別の変数を使ってしまって増やした変数を使い忘れてるのか、それともあとで使うつもりだけど今は使ってないのか、あるいは他に同じインタフェースがあってそのインタフェースに合わせた結果として未使用変数が増えたのか、コミットからは分からない。

linterで警告される

上記のように、未使用変数は「間違って使ってないのか」「他の都合で未使用変数になっているのか」「一時的な変更のせいなのか」が分からないので、linterで静的解析を行うと警告が出る場合がある。

とくにC言語などのコンパイラ言語の場合、「定義されてない変数を使ってる」のはコンパイルエラーになるけど、「定義したけど使ってない変数」はコンパイルエラーにはならないので、linterでチェックすることが多い。 それくらい未使用変数っていうのは問題で、とくにコンパイルでエラーが検出できないので厄介な問題だったりする。 そういうのがあまり知られてない気がする。。。

なお、C言語の場合は「この変数は未使用で問題ない」ということを明示的に示す慣用的な記法もあって、それはvoidにキャストするということ。 こうするとlinterも「あぁ、この変数は使わないでOKなんだな」と理解して警告を出さなくなる。

たとえば、今回の例で最初の一手後のコードをC言語っぽく書くと次のようになる:

char transform_letter(char letter, int char_code) {
    if (is_between(char_code, 'a', 'm') || is_between(char_code, 'A', 'M')) {
        char_code += 13;
    } else if (is_between(char_code, 'n', 'z') || is_between(char_code, 'N', 'Z')) {
        char_code -= 13;
    }
    return (char)char_code;
}

このままlinterにかけると「letterが未使用変数だ」という警告が出る。

それを防ぐには、次のようにする必要がある:

char transform_letter(char letter, int char_code) {
    (void)letter;
    ...

あるいは、マクロを用意しておいて次のようにしたりもする:

#define UNUSED(x) (void)(x)

char transform_letter(char letter, int char_code) {
    UNUSED(letter);
    ...

Pythonの場合もFlake8で確認してみると、引数の未使用は警告されなかったけど(本来は警告されるべき)、ローカル変数で未使用のものは警告された。

なので、たとえば次のようなコードがあったとする:

def clip(value: int) -> int:
    if value >= 10:
        return 10
    return value

この10がマジックナンバーなので、説明変数を導入しようと思ってまずは次のようにしたとする:

def clip(value: int) -> int:
    max_value = 10  # 初手では変数だけ導入
    if value >= 10:
        return 10
    return value

この段階でFlake8でチェックすると、次のような警告が出る:

...: F841 local variable 'max_value' is assigned to but never used

もちろん、次のように直せばこの警告は消える:

def clip(value: int) -> int:
    max_value = 10
    if value >= max_value:
        return max_value
    return value

つまり、未使用変数を導入しただけだとむしろコードは悪化してて、それをちゃんと使うところまでやらないとダメっていうこと。 今回のリファクタリングも都度テストを回して「OK」ってやってたけど、静的解析まで考えると「NG」に近い状況が発生してる (残念ながら引数の未使用はFlake8だとチェックされないっぽいけど)。

オブジェクト指向でもそうなんだけど、とにかく不正な状態を作っちゃダメで、もし作らざるを得ないならコメントとかが必須。

関数の戻り値の未使用

ちょっと関連する話で、関数の戻り値を使用してない場合にもC言語のlinterは警告を出す。 こういうときにもその値を使わないことを示すのにはvoidキャストが使われる。

PythonのFlake8だとこれはチェックしてくれないみたいだけど、安全側に倒すなら本当はチェックした方がいい。

たとえば、次のようなミス、やったことある人もいるのでは?

a = [10, 8, 15]

# ソートした?
sorted(a)

print(a)  # => [10, 8, 15]のまま・・・

これはsorted()はソートしたあとのリストを返すんだけど、元のリストをソートしてくれるわけではないので、こういうことが起きる。 ここで関数の戻り値が未使用という警告が出ていたのなら、次のように直すことができる:

# 戻り値を使うようにしないと
a = sorted(a)

ただ、こうすると今度は使わない関数の戻り値まで警告が出ることになる。 そういった使わない戻り値を明示する方法をPythonはちゃんと持ってて、_ に代入するといい。

_ = some_func(a, b)  # 戻り値は使わない

なので、未使用の引数も本当は_に代入して未使用ということを明示した方がいいのかもしれない:

def transform_letter(letter: str, char_code: int) -> str:
    _ = letter  # 未使用変数の明示
    ...

あるいは、ちょっと意図が分かりにくいのでunused()みたいな何もしないメソッドを用意するとか:

def unused(arg: Any) -> None:
    _ = arg

def transform_letter(letter: str, char_code: int) -> str:
    unused(letter)  # 未使用変数の明示(「unused()に使われてる」扱いになる)
    ...

そんな感じで、未使用変数一つとっても実はいろいろ議論するべき内容がある。

それくらい未使用変数を作るのは問題なんだけど、まぁ普段は未使用変数なんて作ることもないだろうから、それが問題でlinterでのチェック対象になってることや、未使用でもOKだということを明示する方法が知られてない気がする。

今日はここまで!

『Pythonでリファクタリングできなかった話』をLTしてみた。

もう定番になってるけど、今回もLTしたよって話。

発表したスライドは以下:

ただ、今回の発表はちょっと反省が多くて、さすがに内容がマニアックすぎた(^^; 実際にあった話をベースにしてたのでPythonの中級〜上級向けの内容になってしまっていて、Pythonに詳しくない人も多数参加している場にはちょっと合わなかった。 このあたり、なかなか難しい。。。

より一般的な話にするのであれば「インスタンス変数を公開しちゃわないでちゃんとカプセル化しましょう」ってなるんだけど、そうするともう普通の話だし、実例の話からはかなり離れてしまうので、「実際にどういう困ったことが起きたのか」という話ができないんだよなぁ。 そして、Python使いにはちゃんと知っといてもらいたい話でもある。。。

ということで、話す内容自体は間違ってないけど、話す場所を間違えた感はある。 場に合わせてもうちょっとアレンジして、Pythonの踏み込んだ話はまた別の場所でできるとよかったのかな。

ちなみに来週のLTでも話す予定で、そこでは同じくPythonのasyncioの話をしようと思ってたんだけど、今回のことを踏まえるとちょっと変えた方がよさそう。 うーん、何を話すか。。。

今日はここまで!

『技術同人誌はいいぞ』というLTをしてみた。

7/13(水)に、株式会社ラクスさんの主催する勉強会「エンジニアの勉強法ハックLT vol.9」でLT登壇してみた。

話したのは『技術同人誌はいいぞ』というLT。

アウトプットの手段として技術同人誌は面白いので、ぜひいろんな人にチャレンジしてほしい。

ちなみに、LTをしたときの質問で「面白そうだけど何を書けばいいのか分からない、書くものが特にない」というのがあったけど、それに対して「自分が経験したことを書いてみるといい」と回答した。 というのは、経験というのはそれ自体が知見で、こういうところで引っかかったとか、何事も問題なくうまくいったとか、そういうのはマニュアルには書かれていない貴重な情報だから。 それに、技術の解説なら他の人でも書けるかもしれないけど、経験はその人にしか書けないという特徴もある。 経験をふりかえりながら、その中で「あれ?」ってなったところを深掘りできるとさらにいい。

あと、どれくらい時間がかかるかという質問もあった。 これは人にもよるんだけど、1ページが約1,000文字というところからブログの記事数に換算すると目安になるとも伝えた。

ちなみに、作るなら薄い本にした方がよくて、それについては以下を参照:

サークル参加の申し込みについては各イベントの公式ブログの記事を見てほしい:

今日はここまで!

『「サブジェクト」で考えるオブジェクト指向』というLTをしてみた。

6/29(水)に、株式会社ラクスさんの主催する勉強会「オブジェクト指向LT会 vol.4」でLT登壇してみた。

話したのは『「サブジェクト」で考えるオブジェクト指向』というLT。

ここでは設計に関してだけ言及してるけど、主語に注目した方がいいというのはもっと大きな枠でいえると思っていて、それについてはまた別の機会で話したいところ。

今日はここまで!

Pythonの四捨五入で四苦八苦した話。(まとめ)

これまでの各記事は、以下から。

こういった問題を回避するためにも、saneroundを使ってほしい:

今日はここまで!