メモの日々


2023年01月01日(日) [長年日記]

[python] システムのローカルタイムゾーンを持つdatetimeオブジェクトを得る

Pythonでシステムローカルのタイムゾーンを持つdatetimeオブジェクトを得るには、astimezone()メソッドを使う。

引数無し (もしくは tz=None の形 ) で呼び出された場合、システムのローカルなタイムゾーンが変更先のタイムゾーンだと仮定されます。 変換後の datetime インスタンスの .tzinfo 属性には、 OS から取得したゾーン名とオフセットを持つ timezone インスタンスが設定されます。

>>> import datetime
>>> datetime.datetime(2023, 1, 1).isoformat()
'2023-01-01T00:00:00'
>>> datetime.datetime(2023, 1, 1).astimezone().isoformat()
'2023-01-01T00:00:00+09:00'

[python] コマンドラインでのJSONの整形にPythonを使う

以前にPowerShellを使ったJSONの整形をメモしたが、Pythonが使えるならより柔軟に整形できる。

PythonにはコマンドラインでのJSONの整形用にjson.toolモジュールが用意されているのでこれを使う。

$ echo '{"a": 1, "b1": {"b2": {"b3": {"b4": 2}}}, "c": ["hello", "world"]}' | python -m json.tool
{
    "a": 1,
    "b1": {
        "b2": {
            "b3": {
                "b4": 2
            }
        }
    },
    "c": [
        "hello",
        "world"
    ]
}

オプションもいくつか用意されていて助かる。

[python] Pythonの代入式

Python 3.8から、代入した結果を返してくれる代入式が使えることを知った。

>>> print(a := 1)
1

内包表記でも使えて便利。

>>> d = { "hello": "world" }
>>> a = [ "hello", "apple", "pen" ]
>>> [v for x in a if (v := d.get(x)) is not None]
['world']

2023年01月02日(月) [長年日記]

[python] Pythonで例外のスタックトレース情報を得る

Pythonではtraceback.fromat_exc()などで例外のスタックトレースを得られるが、これで得られる文字列は冗長なので、その元データを得たいときがある。

スタックトレースを文字列ではなくデータとして得るには、traceback.extract_tb()を使うのがよさそうだ。

この関数はStackSummaryオブジェクトを返し、これはFrameSummaryの配列だ。FrameSummaryからは、filename, lineno, name, line, locals といった情報を得られる(ドキュメントには見当たらないがソースコードを見るとわかる)。

例外オブジェクトには__traceback__属性があるので、これをextract_tb()に渡せば例外のスタックトレースデータを得られる。

import traceback

def f():
    g()

def g():
    raise RuntimeError()

try:
    f()
except Exception as ex:
    for frame in traceback.extract_tb(ex.__traceback__):
        print(f"{frame.name} at {frame.filename}:{frame.lineno}")
<module> at /home/kenichi/work/python/stacktrace_sample.py:10
f at /home/kenichi/work/python/stacktrace_sample.py:4
g at /home/kenichi/work/python/stacktrace_sample.py:7

2023年01月03日(火) [長年日記]

[python] with文とコンテキストマネージャとcontextlib

Pythonのコンテキストマネージャはすぐにわからなくなってしまうのでメモ。コンテキストマネージャをうまく使えばある種のコードが簡潔に書けるようになる。

なお、関連してasync with非同期コンテキストマネージャもあるがここでは触れない。

with文

with文を使うことで、そこで指定したコンテキストマネージャの __enter__() と __exit__() を暗黙的に呼び出すことができる。

次のドキュメントできちんと説明されているので、わからなくなったらここを読むのがいい。

class C:
    def __enter__(self):
        print("enter")

    def __exit__(self, exc_type, exc_value, exc_tb):
        print("exit")

with C():
    print("hello")
    raise RuntimeError()
enter
hello
exit
Traceback (most recent call last):
  File "/home/kenichi/work/python/context_manager.py", line 10, in <module>
    raise RuntimeError()
RuntimeError

with文のブロック内で例外が投げられたときに C.__exit__() が呼ばれることが重要だ。同じことはtry/finallyで実現できるが、with文を使うとより簡潔に書けるようになる。

コンテキストマネージャ

上述のCクラスのように __enter__() と __exit__() を持つクラスがコンテキストマネージャだ。

__enter__()は戻り値を返すことができ、それはwith文のas節で受け取ることができる。

__exit__()の方は例外を意識する必要がある。

  • 戻り値で真を返すとwith文のブロック内で投げられた例外がwith文の外へ伝播しなくなる。
  • with文のブロックで例外が投げられると、その情報を引数のexc_type, exc_value, exc_tbで受け取る。

contextlib

contextlibモジュールにてwith文用のユーティリティが提供されている。

@contextmanager

@contextmanagerはコンテキストマネージャを関数で定義できるようになるデコレータだ。先の例と同じものを次のように書くことができる。

import contextlib
from collections.abc import Iterator

@contextlib.contextmanager
def f() -> Iterator[None]:
    print("enter")
    try:
        yield
    finally:
        print("exit")

with f():
    print("hello")
    raise RuntimeError()

f()には型ヒントを付けたが、@contextmanagerで修飾する関数はIteratorを返す必要がある。yieldで渡した値がwith文のas節に渡される(上の例には無いけど)。

また、@contextmanagerで作ったコンテキストマネージャは自動的にデコレータにもなり、次のように使えるようになる。

@f()
def g():
    print("world")

g()
enter
world
exit
組み込みのコンテキストマネージャ

contextlibでは色々なコンテキストマネージャを用意してくれている。

  • closing
    • 指定したオブジェクトのclose()を自動的に呼び出してくれる。
  • nullcontext
    • 何もしないコンテキストマネージャ?
  • suppress
    • 指定した例外を捨てる。
  • redirect_stdout
    • 標準出力を一時的に指定先へリダイレクトする。
  • redirect_stderr
    • 標準エラー出力を一時的に指定先へリダイレクトする。
  • chdir
    • カレントディレクトリを一時的に変更する。
  • ExitStack
    • コンテキストマネージャや関数を登録することで複数の後処理を行えるようにする。

2023年01月04日(水) [長年日記]

[python] クラスのインスタンス変数の型ヒント

クラスのインスタンス変数の型ヒントの書き方はPythonのドキュメントから見つけられない。

PEP526内に説明があるということをメモ。


2023年01月05日(木) [長年日記]

[windows] WSLのディストリビューションを非Cドライブに移動した

CドライブでWSLが使用している領域が大きかったので、別のドライブへ移動した。やったことをメモ。

インストールされているディストリビューションの確認

> wsl -l -v
  NAME                   STATE           VERSION
* Debian                 Stopped         2
  docker-desktop-data    Stopped         2
  docker-desktop         Stopped         2

このDebianを移動したい。

エクスポートする

移動するために対象のディストリビューションをエクスポートする。

--vhdオプションを付けた方が速くエクスポートできるようだ。

コマンドを実行したフォルダに巨大なファイルが作られるので注意。

> wsl --export Debian debian.vhdx --vhd
エクスポートが進行中です。これには数分かかる場合があります。
この操作を正しく終了しました。

インポートする

> wsl --import debian2 M:\kenichi\wsl\debian debian.vhdx --vhd
インポート中です。この処理には数分かかることがあります。
この操作を正しく終了しました。

> wsl -l -v
  NAME                   STATE           VERSION
* Debian                 Stopped         2
  docker-desktop-data    Stopped         2
  docker-desktop         Stopped         2
  debian2                Stopped         2

Mドライブへ別名でインポートした。インポート時にも --vhd オプションが必要。

この段階でインポートしたディストリビューションが動くことを確認したらいい。Windows Terminalでdebian2を選べるようになっているはず。

既定のディストリビューションを変更する

今後はdebian2の方を使うので、それを既定に設定する。

> wsl --set-default debian2
この操作を正しく終了しました。

> wsl -l -v
  NAME                   STATE           VERSION
* debian2                Stopped         2
  docker-desktop-data    Stopped         2
  docker-desktop         Stopped         2
  Debian                 Stopped         2

移動元のディストリビューションを削除する

インポートしたディストリビューションが動くことを確認できたら移動元の方は削除する。

これでCドライブの空き容量が増えた。

> wsl --unregister Debian
登録解除。
この操作を正しく終了しました。

> wsl -l -v
  NAME                   STATE           VERSION
* debian2                Running         2
  docker-desktop-data    Stopped         2
  docker-desktop         Stopped         2

既定のユーザーを設定する

インポートしたディストリビューションは起動するとログインユーザがrootになる。これを変更するにはディストリビューション内に /etc/wsl.conf を作るのがいいようだ。日本語の説明はわかりにくかったので英語版から引用。

Warning

This command will not work for imported distributions, because these distributions do not have an executable launcher. You can instead change the default user for imported distributions using the /etc/wsl.conf file.

$ cat /etc/wsl.conf
[user]
default=kenichi

2023年01月19日(木) [長年日記]

[c++] OpenMPのサンプル

OpenMPの使い方を毎回忘却してしまうので例をメモしておく。

並列に偶数のvectorを構築する例。

#include <iostream>
#include <vector>

#include <omp.h>

std::vector<int> make_vector(int size)
{
  std::vector<int> result;

  #pragma omp parallel
  {
    std::vector<int> local;

    #pragma omp for nowait
    for (int i = 0; i < size; ++i) {
      if (i % 2 == 0) local.push_back(i);
    }

    #pragma omp critical
    result.insert(result.end(), local.begin(), local.end());
  }

  return result;
}

int main()
{
  const auto max_threads = omp_get_max_threads();
  std::cout << "max_threads = " << max_threads << std::endl;

  const auto v = make_vector(max_threads * 10);

  std::cout << "v.size = " << v.size() << std::endl;
  for (auto e : v) {
    std::cout << e << ", ";
  }
  std::cout << std::endl;
}
max_threads = 6
v.size = 30
0, 2, 4, 6, 8, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 30, 32, 34, 36, 38, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28,
  • コンパイル時にOpenMPの使用を明示する必要がある。GCCの場合は-fopenmpオプションを付与する。
  • #pragma omp parallelのあるブロックはマルチスレッドで実行される。ブロック内で宣言された変数はスレッドローカルな変数になる。ブロックの外で宣言された変数はスレッド間で共有する変数になる(のでアクセス時に排他制御が必要)。
  • #pragma omp forのあるforループはスレッド間で分担して実行される。デフォルトではループの末尾で全スレッドが待ち合わせるが、nowaitを指定すると待ち合せなくなる。
  • #pragma omp criticalのあるブロックはスレッド間で排他的に実行される。

上の例のmake_vector()は、#pragma omp declare reductionを使ってリダクション識別子を宣言すると明示的にcriticalを指定せずに書くことができる。ただ、コードが分かりにくくなるので積極的に使うべきかは微妙。

#pragma omp declare reduction ( \
  concat :  \
  std::vector<int> :  \
  omp_out.insert(omp_out.end(), omp_in.begin(), omp_in.end()) \
)

std::vector<int> make_vector(int size)
{
  std::vector<int> result;

  #pragma omp parallel for reduction(concat: result)
  for (int i = 0; i < size; ++i) {
    if (i % 2 == 0) result.push_back(i);
  }

  return result;
}
  • concatという、vectorを結合するリダクション識別子を宣言した。その内容を定義する際にはomp_inとomp_outという識別子を使うと決まっている。
  • 宣言したリダクション識別子は、ディレクティブのreduction節で使用する。reduction節ではリダクション識別子にスレッド間の共有変数(上の例ではresult)を適用する。
    • これにより、当該ブロック内での当該共有変数に対する操作はスレッドローカルに行われるようになる。
    • ブロックを抜ける際に、スレッドローカルな値が共有変数へ定義したリダクション内容で反映される。
    • (と理解した)
  • #pragma omp parallel forは最初の例で使った #pragma omp parallel と #pragma omp for をまとめた処理を行う。criticalを使わなくてよくなるのでこのディレクティブを使える。

2023年01月31日(火) [長年日記]

[dev] ROS2でFast-DDSの設定を変更する

ROS2というミドルウェアを使っている。現在の最新のリリースバージョンは「Humble」。

ROS2で通信回りの動作をカスタマイズしようとすると、その下位レイヤであるDDSの設定を変更しなければならなくなる(ROS2のAPIとして整備されていないことが多い)。

ROS2でのFast DDS設定変更方法

ROS2が使うDDSの実装は変更できるが、現在のデフォルトはFast DDSである。ROS2においてFast DDSの設定を変更する方法は次のドキュメントに書かれている。

やることは次の通り。

  • Fast DDSの設定を書いたXMLファイルを作成する。XMLファイルについては11. XML profilesに説明があり、色々な設定ができる。
  • 環境変数 FASTRTPS_DEFAULT_PROFILES_FILE にXMLファイルのパスを設定する。
  • 環境変数 RMW_FASTRTPS_USE_QOS_FROM_XML を 1 に設定する。

XMLファイル使用時に発生するエラーとその解決方法

しかし、ドキュメントに例示されているXMLファイルを用意してもエラーになってしまう!

発生するエラーは次の2種類。

  • eprosima::fastcdr::exception::NotEnoughMemoryException 例外が投げられて終了する
  • 次のようなエラーメッセージがたくさん出力される
 [RTPS_READER_HISTORY Error] Change payload size of '68' bytes is larger than the history payload size of '35' bytes and cannot be resized. -> Function can_change_be_added_nts

困っていたが、試行錯誤の結果次のようにすると解決することが分かった。

  • 前者のエラーは、XMLにis_default_profile属性をtrueに設定した<data_writer>を記述してその中で<historyMemoryPolicy>をPREALLOCATED以外に設定すれば解決する。
  • 後者のエラーは、XMLにis_default_profile属性をtrueに設定した<data_reader>を記述してその中で<historyMemoryPolicy>をPREALLOCATED以外に設定すれば解決する。

つまり、次のようなXMLファイルを用意すればいい。

<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
    <data_writer profile_name="default writer profile" is_default_profile="true">
        <historyMemoryPolicy>PREALLOCATED_WITH_REALLOC</historyMemoryPolicy>
    </data_writer>

    <data_reader profile_name="default reader profile" is_default_profile="true">
        <historyMemoryPolicy>PREALLOCATED_WITH_REALLOC</historyMemoryPolicy>
    </data_reader>

    <!-- 以下に必要な設定を追加する -->

</profiles>

なお、<data_writer>は<publisher>、<data_reader>は<subscriber>とも書ける。Fast DDSのドキュメントは全体的に<data_writer>, <data_reader> を使うように書き換えられているようなので、こちらを使うべきなのだと思う。

ドキュメントを修正してもらう

ドキュメントの通りにやってエラーになるのはおかしいので、修正するpull requestを出した。

そうしたら、最新版(Rolling)ではエラーにならないと言われてしまった。

調べると、Fast DDS 2.9.0

Default memory management policy set to PREALLOCATED_WITH_REALLOC_MEMORY_MODE (#3108)

というBehavior changesが入っていて、恐らくこの変更によりRollingではエラーにならないのだと思う。

まとめ

  • ROS2の現状のリリース版でFast DDSの設定を変更する際には、XMLファイルにis_default_profile属性がtrueの<data_writer>と<data_reader>を記述してそれぞれのhistoryMemoryPolicyをPREALLOCATED以外に設定する必要がある。
  • Fast DDSのバージョンが2.9.0以降であれば上の設定をしなくてもよくなりそう。自分では未確認。