delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

iTerm2で起動時のディレクトリを設定する

ターミナルにiTerm2を使っています。

iTerm2ではターミナル起動時の初期ディレクトリを設定できます。よく使うディレクトリがある場合は設定しておくと便利です。

設定方法

Settingsを開きます。

Profilesを開きます。デフォルトはHome directoryになっています。

Directoryを選択してよく使うディレクトリを設定します。これでOK

Macでペアワイズ法サポートツールのPICTを使う

Macでペアワイズ法サポートツールのPICTを使ってみます。 github.com

PICTはWindows環境用のツールであり、Macで実行できるようにするのは面倒そうという先入観がありました。

が、今日ではbrewで提供されているので簡単にインストールできました。

formulae.brew.sh

brew install pict

文字コード問題

以下のようなインプット用のファイルを作成します。

input.txt

インド料理: バターチキン, サンバル, マサラドーサ
ネパール料理: チョエラ, スクティ, モモ
スリランカ料理: パリップ, ポルサンボル, ワタラッパン

使い方的にはこれでpict input.txtを実行すればいいはずなのですが、生成結果が意図しないものになってしまいました。

調べたところ、「日本語が含まれる際は文字コードEUCにしないと化ける」という情報がありました。

techblog.kayac.com

従って、以下のようにUTF-8EUC-JPに変換して渡すことで解決しました。ChatGPTに教えてもらったのですが、「プロセス置換」という方法でワンライナーで書けるようです。

pict <(iconv -f UTF-8 -t EUC-JP input.txt)

文字コード問題は2台のMacの内の片方でしか発生せず、発生条件が謎なままです。(もう一台はUTF-8のまま問題なく読み込めた)

結果

以下のようにパターンを生成できました!

インド料理  ネパール料理  スリランカ料理
マサラドーサ  スクティ    ポルサンボル
サンバル    スクティ    パリップ
マサラドーサ  モモ  ワタラッパン
サンバル    チョエラ    ワタラッパン
サンバル    モモ  ポルサンボル
バターチキン  スクティ    ワタラッパン
マサラドーサ  チョエラ    パリップ
バターチキン  チョエラ    ポルサンボル
バターチキン  モモ  パリップ

Rubyのハッシュとキーワード引数の省略記法

Ruby3.1からハッシュとキーワード引数の省略記法が使えます。

仕事でコードレビューをしていて「これは何だ?」と思ってしまったので確認しておきたいと思います。

www.ruby-lang.org

ハッシュの場合

事前にc = 3のようにローカル変数を定義している場合、{ c: }と書くだけで{c: 3}と同じ意味になります。

mapping1 = {
  a: 1,
  b: 2
}
pp mapping1 # {a: 1, b: 2}

c = 3
mapping2 = {
  c:,
  d: 4
}
pp mapping2 # {c: 3, d: 4}

キーワード引数の場合

事前にname = "田中"のようにキーワード引数と同じ名前のローカル変数を定義している場合、hello(name:)と書くだけでhello(name: name)と同じ意味になります。

def hello(name:)
  puts "こんにちは#{name}さん!"
end

hello(name: "山田") # こんにちは山田さん!

name = "田中"
hello(name:) # こんにちは田中さん!

参考

qiita.com

WEB+DB PRESS Vol.127』を読むとこの省略記法が導入された背景もわかって理解が深まります。

gihyo.jp

RubyのStringクラスに絵文字変換メソッドを追加してみる(オープンクラスの勉強)

Rubyの特徴であるオープンクラスの勉強のため、Stringクラスに絵文字変換メソッドを追加してみます。

例えば、"カレー".to_emojiを実行すると🍛がreturnされるイメージです。

実装

以下のように簡単に実装できました。

class String
  def to_emoji
    case self
    when "カレー"
      "🍛"
    when "ラーメン"
      "🍜"
    when "寿司"
      "🍣"
    else
      self
    end
  end
end

Stringインスタンス自身を参照するためにはselfが使えます。

docs.ruby-lang.org

実行結果

# ※ class定義に続けて実行

puts "カレー".to_emoji # 🍛
puts "ラーメン".to_emoji # 🍜
puts "寿司".to_emoji # 🍣
puts "牛丼".to_emoji # 牛丼 ※ 対応する絵文字が定義されていないため、「牛丼」がそのまま返される
puts "curry".upcase # CURRY ※ 既存のRubyのStringクラスの挙動が破壊されていないことの確認

Rubyの「=で終わるメソッド」が代入風に書けるのは言語仕様なのか?

課題

Rubyでは、フィールド名+=というメソッド名を定義することで、インスタンス変数への代入のような表現ができます。

以下のサンプルコードでいうと、def name=(name)がそれにあたります。

class Person
  def initialize(name)
    @name = name
  end

  def name=(name)
    @name = name
  end

  def introduce
    puts "私の名前は#{@name}です"
  end
end

p = Person.new("山田")
p.introduce # 私の名前は山田です

p.name = "田中"
p.introduce # 私の名前は田中です

このような書き方が可能なのは、次のどちらの理由によるものなのか気になったので調べてみました。

  1. Rubyではメソッド名に=が使えるため、これは単なるRuby文化的な慣習にすぎない
  2. Rubyの言語仕様としてメソッドの末尾が=の場合が特別にサポートされている

結論

答えは2.でした。Rubyのパーサーには、末尾が=で終わるメソッド専用の処理ロジックが組み込まれていました。

推論

最終的にはRubyのパーサーのコードを確認して裏付けを取りましたが、挙動の観察だけでも2.の可能性が高いと推測できます。

p.name=("田中")の場合

メソッド名がname=なので、当然ながら次のようにも書けます。

# 省略
p.name=("田中")
p.introduce # 私の名前は田中です

p.name= "田中"の場合

Rubyではメソッド呼び出しのかっこを省略できるため、次のように書いても動作します。

# 省略
p.name= "田中"
p.introduce # 私の名前は田中です

ここまでは、1.と2.のどちらの仮説でも説明がつきます。

p.name = "田中"の場合

しかし、p.name=の間にスペースが入るこの書き方も動作します。

# 省略
p.name = "田中"
p.introduce # 私の名前は田中です

つまり、Rubyp.name =p.name=として解釈していることになります。

Rubyには「メソッド名の途中にスペースが入っても自動で補完する」といった仕様は存在しないため、この挙動は通常のメソッド呼び出しルールでは説明できません。

たとえば、name=メソッドの末尾を?に書き換えて同様のことを試してみると、syntax error, unexpected tIDENTIFIER, expecting ':'というエラーになります。

class Person
  # 省略
  def name?(name)
    @name = name
  end
  # 省略
end
# 省略
p.name ? "田中"
p.introduce # 私の名前は田中です

このことから、Rubyには末尾が=で終わるメソッドには特別な構文ルールが存在すると推測できます。

裏付け

裏を取るためにChatGPTにナビゲーションしてもらいながら、Rubyのパーサーのソースコードを読んでみました。

ChatGPTによると、Ruby本体のリポジトリに同封されているパーサーよりもRuby3.4からデフォルトになったPrismパーサーのソースコードを読んだほうがいいそうです。

github.com

参考記事

gihyo.jp gihyo.jp

src/prism.c内のparse_write関数を見ると、以下のような実装が確認できます。

parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_node_t *value) {
    // 省略
    switch (PM_NODE_TYPE(target)) {
        // 省略
        case PM_CALL_NODE: {
                // 省略
                if (char_is_identifier_start(parser, call->message_loc.start, parser->end - call->message_loc.start)) {
                    // When we get here, we have a method call, because it was
                    // previously marked as a method call but now we have an =. This
                    // looks like:
                    //
                    //     foo.bar = 1
                    //
                    // When it was parsed in the prefix position, foo.bar was seen as a
                    // method call with no arguments. Now we have an =, so we know it's
                    // a method call with an argument. In this case we will create the
                    // arguments node, parse the argument, and add it to the list.
                    pm_arguments_node_t *arguments = pm_arguments_node_create(parser);
                    call->arguments = arguments;

                    pm_arguments_node_arguments_append(arguments, value);
                    call->base.location.end = arguments->base.location.end;

                    parse_write_name(parser, &call->name);
                    pm_node_flag_set((pm_node_t *) call, PM_CALL_NODE_FLAGS_ATTRIBUTE_WRITE | pm_implicit_array_write_flags(value, PM_CALL_NODE_FLAGS_IMPLICIT_ARRAY));

                    return (pm_node_t *) call;
                }
            }
            // 省略
        }
       // 省略
    }
}

私はパーサーのコードを読み慣れてない & C言語に疎いのでロジックの詳細は分かりませんが、コードコメントからこのブロックが末尾が=で終わるメソッド専用のロジックであることが伺えます。

さらに、parse_writePM_CALL_NODEのブロックで呼び出しているparse_write_nameを読むと、メソッド名の末尾に=を追加する処理をやっていそうです。

parse_write_name(pm_parser_t *parser, pm_constant_id_t *name_field) {
    // The method name needs to change. If we previously had
    // foo, we now need foo=. In this case we'll allocate a new
    // owned string, copy the previous method name in, and
    // append an =.
    pm_constant_t *constant = pm_constant_pool_id_to_constant(&parser->constant_pool, *name_field);
    size_t length = constant->length;
    uint8_t *name = xcalloc(length + 1, sizeof(uint8_t));
    if (name == NULL) return;

    memcpy(name, constant->start, length);
    name[length] = '=';

    // Now switch the name to the new string.
    // This silences clang analyzer warning about leak of memory pointed by `name`.
    // NOLINTNEXTLINE(clang-analyzer-*)
    *name_field = pm_constant_pool_insert_owned(&parser->constant_pool, name, length + 1);
}

以上より、Rubyのパーサーは「foo.bar = 1のような式を見つけたら、bar=というメソッド呼び出しとして扱う」という専用処理を持っていることが確認できました。

django-auditlogで特定のユーザー種別の場合にログを抑制する方法

概要

django-auditlogというjazzbandの管轄のDjangoのモデル操作の監査ログを作成してくれるライブラリがあります。

github.com

基本的な使い方は以下の記事でryu22eさんが紹介してくださっているので割愛します。

qiita.com

モデルを監視してCRUD操作を検知する思想なので、モデル単位でのログ抑制は設定で可能です。

ただ今回はユーザー種別の場合にログを抑制したいというニーズがあり、やり方が分からなかったので技術検証してみます。

ChatGPTに聞いてみた

前提としてググってもdjango-auditlogの発展的な使い方に関する情報はほとんど見つかりませんでした。

目ぼしいものは8年前のものですが以下のStack Overflowの記事くらいでした。この記事も前提となるDjangoのバージョンが1.8かつdjango-auditlogも2023年に3系にメジャーバージョンアップしているのでそのまま使える情報ではなさそうです。

stackoverflow.com

ChatGPTに聞いてみたところ、以下のような回答でした。

ChatGPTの回答

①カスタム Middleware を定義する

your_app/middleware.py

from auditlog.middleware import AuditlogMiddleware

class CustomAuditlogMiddleware(AuditlogMiddleware):
    def get_user(self, request):
        user = super().get_user(request)

        # 例: User モデルに user_type フィールドがある場合
        if getattr(user, 'user_type', None) in ['system', 'batch']:
            return None  # ログに記録しないようにする

        return user

settings.pyで Middleware を差し替える

MIDDLEWARE = [
    # 'auditlog.middleware.AuditlogMiddleware',  ← 削除
    'your_app.middleware.CustomAuditlogMiddleware',  # ← 追加
    # その他のミドルウェア
]

AuditlogMiddleware_get_actorのオーバーライドを試す

ChatGPTの回答の裏取りで最新のAuditlogMiddlewareソースコードをみてみたところ、get_userというメソッドはありませんでしたが、恐らく_get_actorのことを指しているのだろうと読み替えました。

github.com

以下のように_get_actorをオーバーライドして、元の実装に追加でuser.is_staff = Trueの場合はNoneを返すようにしてみました。

from auditlog.middleware import AuditlogMiddleware
from django.contrib.auth import get_user_model

class CustomAuditlogMiddleware(AuditlogMiddleware):
    
    @staticmethod
    def _get_actor(request):
        user = getattr(request, "user", None)
        if not isinstance(user, get_user_model()):
            return None
        if not user.is_authenticated:
            return None
        if user.is_staff:
            return None
        return None

結果

USERsystemとなっただけでログは作成されてしまい、期待通りの挙動にはなりませんでした。

_get_actorNoneを返した場合、USERsystemになるだけでログ自体は作成される仕様のようです。

AuditlogMiddleware__call__のオーバーライドを試す①

AuditlogMiddlewareをオーバーライドする方針はそのままで、__call__の方を以下のようにオーバーライドしてみました。

from auditlog.middleware import AuditlogMiddleware
from auditlog.cid import set_cid
from auditlog.context import set_actor

class CustomAuditlogMiddleware(AuditlogMiddleware):
    
    def __call__(self, request):
        remote_addr = self._get_remote_addr(request)
        remote_port = self._get_remote_port(request)
        user = self._get_actor(request)

        if user and user.is_staff: # 追加: ユーザーがスタッフの場合は監査ログを抑制する
            return self.get_response(request)

        set_cid(request)

        with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port):
            return self.get_response(request)

結果

これでもUSERsystemとなっただけでログは作成されてしまい、期待通りの挙動にはなりませんでした。

set_actorはあくまでログに操作したユーザーの情報を付与するだけの役割であって、with set_actorを実行しないようにするだけではログは抑制できないようです。

https://django-auditlog.readthedocs.io/en/latest/usage.html#set-actor

AuditlogMiddleware__call__のオーバーライドを試す②

公式ドキュメントをみていたらset_actorと同様にwithブロックで使えるdisable_auditlogという関数を発見しました。

https://django-auditlog.readthedocs.io/en/latest/usage.html#disable-auditlog

__call__の中でこれを実行するようにしてみます。

from auditlog.middleware import AuditlogMiddleware
from auditlog.cid import set_cid
from auditlog.context import set_actor, disable_auditlog # disable_auditlogのインポートを追加

class CustomAuditlogMiddleware(AuditlogMiddleware):
    
    def __call__(self, request):
        remote_addr = self._get_remote_addr(request)
        remote_port = self._get_remote_port(request)
        user = self._get_actor(request)

        if user and user.is_staff: # ユーザーがスタッフの場合は監査ログを抑制する
            with disable_auditlog():
                return self.get_response(request)

        set_cid(request)

        with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port):
            return self.get_response(request)

結果

この方法でuser.is_staff = Trueの場合にログを抑制することに成功しました。

Deep Researchにスギ花粉が飛ばない旅行先を探してもらった

課題

スギ花粉シーズンに花粉が飛ばない旅行先を調べると小笠原諸島、沖縄、北海道など簡単には行けないところばかり出てくる。

例外として草津が紹介されている記事を見かけたので理由を調べたところ、スギ花粉は標高1000m以上ではあまり飛ばないらしい。草津は標高1200m以上なのでスギ花粉が飛ばないという理由だった。

ただ、標高1000m以上の旅行先というのは意外と他に探すのが難しい。例えばパッと思いつく標高が高そうな温泉街を調べたところ

とのことだった。

ちょうどDeep Researchが話題になっているので、草津以外の標高の観点で花粉の避粉地としておすすめの旅行先を探してもらうのがお題としていいのでは?と考えた。

やったこと

まずは以下を入力してみた。

スギ花粉シーズンに、標高1,200mの草津のように標高1000m以上で花粉が飛ばない旅行先を調べてください。花粉シーズンの旅行先を調べると、草津以外だと北海道や離島や国外を紹介する記事ばかりで、「そこまではいけないな」と困っています。草津以外にもスギ花粉が飛ばないくらい標高が高い旅行先はあるのではないかと思っています。とはいえ、標高が高そうなイメージの温泉地帯を調べると、

- 箱根: 約100m
- 下呂温泉: 300m
- 野沢温泉: 600m

と、意外と1000m以上という条件を満たす旅行先を見つけるのも難しいなと感じています。

すると、以下のようにより詳細な要件を求められた。

草津以外で標高1,000m以上に位置し、スギ花粉が少ない旅行先をお探しですね。

より的確な候補を見つけるために、以下の点を教えてください。

1.エリアの希望:関東圏、甲信越、関西など、特定の地域の希望はありますか?
2.目的:温泉地を中心に探しますか?それとも景観や観光を楽しめる場所ならOKですか?
3.宿泊施設の有無:宿泊できる施設がある場所がよいですか?
4.アクセス:車が必要な場所でも大丈夫ですか?公共交通機関で行けるほうがよいですか?
この情報があれば、より適した候補地をリストアップできます!

以下のように回答した。

1. 関東を中心に青森から近畿地方までくらいならOKです。関東に近いほどうれしいです。
2. 温泉地を中心に探します
3. 宿泊したいです
4. 公共交通機関で行けるほうがよいです

体感10分〜20分くらい調べたのち、リストアップしてくれた旅先は以下だった。

たしかに裏をとってみると、全て標高1000m以上という条件を満たした温泉街であり、最後の青森を除くと関東からアクセスしやすい場所を選んでくれている。

「他にもありますか?」と聞くと、追加で以下を挙げてくれた。

全部長野県だったので「長野県以外もありますか?」と聞くと以下を挙げてくれた。

たしかに全部実際に裏をとってみると実在する温泉街で標高も1000m以上だった。すごい。

Deep Researchは一定期間に10回までしか使えないという制約があるようだが、初回の調査結果に対する追加質問の際は回数は減らないっぽい。