何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

辞書のリストから重複した辞書を取り除く

困った時のitertools

Pythonにおいて、リストから重複を取り除く手段としてよく取り上げられるのが集合set()に渡して取り除く方法である。

>>> a = [1, 3, 2, 3, 6, 2, 5]
>>> a = list(set(a))
>>> a
[1, 2, 3, 5, 6]

この手法の欠点は、集合の要素はハッシュ可能である必要がある。 つまり、上記の例のようなintstrならば集合で十分目的を達成できるが、 リストや辞書のようなハッシュ可能ではないオブジェクトからなるリストの重複を取り除くには工夫が必要である。 直近の業務で遭遇したのがこのような状況である。

レシピを調べる

この手の「リスト全体をなめて云々」するケースでまず調べるべきモジュールはitertoolsモジュールである。 大抵のケースでモジュールのツールそのものか、ドキュメントのレシピ集にその答えがある。 今回のケースでは、レシピ集のunique_everseenで解決することがわかった。

以下は、unique_everseenを大いに参考にして実際に投入したヘルパー関数である。

from typing import List, Dict, Generator


def unique_dictionaries(list_of_dict: List[Dict]) -> Generator[Dict, None, None]:
    """辞書のリストから重複を取り除くジェネレータ"""
    seen = set()
    for element in list_of_dict:
        k = tuple(element.items())
        if k not in seen:
            seen.add(k)
            yield element

unique_everseenは任意のイテレータに対して動作するように書かれているが、今回は「辞書のリスト」とわかっているのでそれがわかるように単純化した。 また、「集合の要素はハッシュ可能」を踏まえ、辞書の中身をハッシュ可能なオブジェクトであるタプルに変換する処理を追加した。

>>> data = [{"a": 1, "b": 2}, {"a": 1, "b": 1}, {"a": 3, "b": 2}, {"a": 1, "b": 2}, {"a": 1, "b": 1}]
>>> assert list(unique_dictionaries(data)) == [{"a": 1, "b": 2}, {"a": 1, "b": 1}, {"a": 3, "b": 2}]  # should be True

重複という概念が成立するのであれば、個々のオブジェクトに応じて何かしらハッシュ可能なオブジェクトに変換する方法があるので応用範囲はそれなりに広いはずである。

『 Linuxプログラミングインタフェース』を気合いで読む会 第7回:6章プロセス:環境変数の編集

スタックとヒープの違いが見抜ける人でないと(putenv()を使うのは)難しい

Linuxプログラミングインタフェース』6章の環境変数の続き。 C言語環境変数を扱うための関数はstdlib.hに用意されているが、その中でも扱いが難しいのがputenv()である。 int putenv(char *string);という形式で、char *stringの値がコピーされるのではなく、グローバル変数environの中の要素が*stringの値を指すという仕様である。 つまり、スタック上にある文字列を渡してしまうと関数が終わった後に別の値に上書きされてしまう。 C言語の場合、スタックとヒープの違いをよくわかっていないとよくある関数すら使いこなせないのである。 厳しい世界である。 なお、CERT セキュアコーディングスタンダードでも注意喚起がされている。

サンプルプログラムmodify_env.c の仕様は以下の通り。サンプルの意図としては、6.7節で説明したgetenv(), putenv(), setenv(), unsetenv(), clearenv()を全て使う例である。

Python

  • os.environが辞書なので辞書に対する操作をそのまま適用すれば良い。
  • os.putenv()などは使わない。
import argparse
import os


def modify_env(environs):
    os.environ.clear()
    for arg in environs:
        key, value = arg.split("=")
        os.environ[key] = value

    if "GREET" not in os.environ:
        os.environ["GREET"] = "Hello world"

    if "BYE" in os.environ:
        del os.environ["BYE"]

    for key, value in os.environ.items():
        print(f"{key}={value}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("environs", nargs="*")
    args = parser.parse_args()
    modify_env(args.environs)


if __name__ == "__main__":
    main()

実行結果は以下の通りである。

(venv) $> python3.7 modify_env.py GREET="Guten Tag" SHELL=/bin/bash BYE=Ciao
GREET=Guten Tag
SHELL=/bin/bash
(venv) $> python3.7 modify_env.py SHELL=/bin/bash BYE=byebye
SHELL=/bin/bash
GREET=Hello world

Rust

  • std::envにある関数を活用すれば良い。
  • 環境変数の有無のチェックはstd::env::var()で行う。Result型なのでうまく処理する必要がある。
  • コマンドライン引数の解析がちょっとぎこちない。
use std::env;

fn modify_env(args: &Vec<String>) {
    for (key, _) in env::vars() {
        env::remove_var(key);
    }

    for arg in args {
        let splited_arg: Vec<_> = arg.split('=').collect();
        let (key, value) = (&splited_arg[0], &splited_arg[1]);
        env::set_var(key, value);
    }

    if env::var("GREET").is_err() {
        env::set_var("GREET", "Hello world");
    }

    if env::var("BYE").is_ok() {
        env::remove_var("BYE");
    }

    for (key, value) in env::vars() {
        println!("{}={}", key, value);
    }
}

fn main() {
    let mut _args = Vec::new();

    for arg in env::args().skip(1) {
        _args.push(arg);
    }
    modify_env(&_args);
}

実行結果は以下の通りである。

$> target/debug/modify_env GREET="Guten Tag" SHELL=/bin/bash BYE=Ciao
GREET=Guten Tag
SHELL=/bin/bash
$> target/debug/modify_env SHELL=/bin/bash BYE=byebye
SHELL=/bin/bash
GREET=Hello world

感想

  • C言語putenv()の扱いが難しい。
  • 他の言語ではそれなりに扱いやすくなっているが、良からぬことが起きたりしないだろうかと心配してしまう。

『 Linuxプログラミングインタフェース』を気合いで読む会 第6回:6章プロセス:環境変数の表示

環境変数の環境とは

Linuxプログラミングインタフェース』6章では環境変数も扱われている。 プロセスごとに環境変数が設定されていて、Linux上では/proc/PID/environから、C言語上ではグローバル変数**environから取得する。

Python

  • os.environに辞書形式で取得できる。
  • 公式ドキュメント曰く、起動時のプロセスの環境変数が反映される。
  • Pythonプログラム内で環境変数を変更する際はos.environを直接編集するとPython側がよしなに対応してくれる。os.putenv()で変更してもos.environの値は変更されない。
import os


def main():
    os.putenv("FOO", "BAR")
    os.environ["HOGE"] = "PIYO"
    for key, value in os.environ.items():
        print(f"{key}={value}")


if __name__ == "__main__":
    main()

実際に振る舞いを確かめてみる。

(venv) $> python3.7 display_env.py | grep FOO  # 出ない!
(venv) $> env FOO=BAR python3.7 display_env.py | grep FOO
FOO=BAR
(venv) $> python3.7 display_env.py | grep HOGE
HOGE=PIYO

Rust

  • std::env::vars()から取得できる。
  • 公式ドキュメント曰く、プログラム起動時(コンパイル時ではない)の環境変数を取得する。
  • 環境変数の編集をするにはstd::env::set_var()std::env::remove_var()関数、キーを指定して取得する場合はstd::env::var()関数を使う。
use std::env;

fn main() {
    let key = "FOO";
    env::set_var(key, "BAR");
    for (key, value) in env::vars() {
        println!("{}={}", key, value);
    }
}

実際に振る舞いを確かめてみる。

(venv) $> target/debug/display_env  | grep FOO
FOO=BAR
(venv) $> env HOGE=PIYO target/debug/display_env | grep HOGE
HOGE=PIYO

感想

  • The Twelve Factor Appを関わっている案件で導入して以来、環境変数との密接な付き合いが続いているが、環境変数の環境がどの範囲を示すのかを理解していなかった。環境とはプロセスごとにある。
  • プログラム自体は大したことがないが、「プロセスごとに環境変数を持っている」という事実は今後の開発にものすごく役に立ちそうな気がしている。それだけ、今まで、ふわっと開発してきたことがわかる。