nikkie-ftnextの日記

イベントレポートや読書メモを発信

同時実行数を制限できるセマフォ、イベントループによる並行処理でも使えるのか!(Pythonのasyncio.Semaphoreの素振り)

はじめに

俺もー!!ノ nikkieです。

Pythonのasyncio(イベントループによる並行処理)まわりで長い間宿題だった事項に、ついに答えを得ました!

目次

前提:asyncioを使った私の実装の伸びしろ

Pythonasync/awaitを使った実装には分かっていない点がありました。

上のコードは全部一度にgatherするので、tweetsが千や万あるとすると、短時間に大量のリクエストが立て続けに送られるように思われます。

単純なサンプルコードで補足します。

% python -V
Python 3.11.8
import asyncio


async def single_request(i: int) -> int:
    print("start", i)
    await asyncio.sleep(i)
    print("end", i)
    return i


async def main():
    return await asyncio.gather(*[single_request(i) for i in range(5, 0, -1)])


if __name__ == "__main__":
    print(asyncio.run(main()))

コルーチンsingle_request()は、引数i秒だけ待ってからiを返します。
HTTP通信のイメージです。
これを5個の引数で呼び出します:

>>> list(range(5, 0, -1))
[5, 4, 3, 2, 1]

5つのコルーチンオブジェクトをasyncio.gather()並行実行します。
https://docs.python.org/ja/3/library/asyncio-task.html#asyncio.gather

start 5
start 4
start 3
start 2
start 1
end 1
end 2
end 3
end 4
end 5
[5, 4, 3, 2, 1]

待ち(asyncio.sleep())に入ると、次のコルーチンオブジェクトを実行するように動くので、5 -> 4 -> 3 -> 2 -> 1 と実行されています。
待ち時間が短いコルーチンオブジェクト(i=1)から返り値が返っていきますが、asyncio.gather()

返り値の順序は、 aws での awaitable の順序に相当します。

そのため、[5, 4, 3, 2, 1]となっているわけです。

今はrange(5, 0, -1)ですが、5を1000や10000にしたときに「全部実行される状態になるよね」という点を懸念に思っていました1
どう解決すればよいか長らくわかっていませんでした2が、セマフォを使って対処できることを知りました。

概念は知っていたセマフォ

「HTTP通信処理にtime.sleep()を入れるの、なんかカッコ悪いな〜(もっといいやり方あるんじゃないか)」と私は美意識上の引っ掛かりを感じていたので、セマフォthreading.Semaphore)の存在はとても興味を惹かれました(ふおおおお!)
https://docs.python.org/ja/3/library/threading.html#semaphore-example

セマフォを使って、同時処理数を制御する

「asyncioでもセマフォ使えるよ」と教えていただいたのがこちらの記事。

並列数の制限にはSemaphoreを使います。

こちらを参考に、上記のサンプルコードを変更します。

実行すると、非同期処理であっても、それに対応するセマフォによって同時処理数が制限されているんです!!

start 5
start 4
end 4
start 3
end 5
start 2
end 2
end 3
start 1
end 1
[5, 4, 3, 2, 1]

最初に表示されるのは「start 5」「start 4」なので同時に処理されるのは2つなんです!(Semaphore(2)

なお、async with semaphore:の行をコメントアウトすると、同時処理数の制限がない状態(=記事最初で示した状態)です

セマフォの書き方素振りゾーン

上は関数内関数(正確には、コルーチン内コルーチン?)でしたが、別の書き方も試します。

こちらはクラスの属性にasyncio.Semaphoreを持たせる例。

このZennの記事からセマフォインスタンスを引数で渡せるとヒントを得て、コルーチンを抽出しました。

-async def main():
-    semaphore = asyncio.Semaphore(2)
-
-    async def coroutine(i: int) -> int:
-        async with semaphore:
-            return await single_request(i)
-
-    return await asyncio.gather(*[coroutine(i) for i in range(5, 0, -1)])
+async def coroutine(sem, i: int) -> int:
+    async with sem:
+        return await single_request(i)
+
+
+async def main():
+    semaphore = asyncio.Semaphore(2)
+    return await asyncio.gather(
+        *[coroutine(semaphore, i) for i in range(5, 0, -1)]
+    )

関数内関数を解消して同様に動かせました(リファクタリング成功🙌)

終わりに

Pythonでasync/awaitによる並行処理の実行数を制御できるasyncio.Semaphoreを知りました。
HTTP通信の並行処理の最後のピースがハマった感があります。
並行処理でもセマフォで制限をかけられるんだ!
よ う や く 完全に理解したぞ!

しかし、コンテキストマネージャーとして使うだけで処理数に制限がかかるセマフォ、いったい全体どういう仕組み(実装)なんでしょう?


  1. 直面した記事です。
  2. 過去の記事では、more_itertoolsでchunkに分けて、chunkごとにasyncio.gather()のようなことをしています

uvお試し記:uv venvで作った仮想環境でpip installしてはいけません。uv pip installしましょう

はじめに

銃を抜いたからには 命を懸けろよ、nikkieです。

uvを試したところ、「思ってたのと、違う!」となった(=私が誤解していた)点のメモ書きです。
学び:uv付けたならuv付け通せよuvカットはできません)

目次

前提:Python Monthly Topicsより、uv使ってみよう

venv + pip と uv の比較記事です1
uvの速度について言及されています。

  • python -m venv vs. uv venv
  • pip install vs. uv pip install

uvで仮想環境を作ったら、続くコマンドにもuvを付け続ける

macOSbrewで入れました2

% uv --version
uv 0.1.31

📌こちらの使い方をしていきましょう(コマンドにはuvを付け続ける)

% uv venv
% uv pip install kojo-fan-art

(仮想環境を有効にしていないですが、)これでuvが作った仮想環境にインストールできます3

% source .venv/bin/activate
(work) % kojo-day

(work) % python -q
>>> import the_solitary_castle_in_the_mirror

uvが作る仮想環境は、venvが作る仮想環境と同じと考えてはいけない

uvが作る仮想環境とvenvが作る仮想環境を同じと誤解したために小さくハマりました。

uv venvは仮想環境の有効化を促します4
(私の環境ではpyenvが管理するPythonが選択されています)

Using Python 3.10.9 interpreter at: /.../.pyenv/versions/3.10.9/bin/python3
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate

私がやってしまったのは、有効化してからのpip install(俺みたいになるな!)

% rm -rf .venv
% uv venv
% source .venv/bin/activate

(work) % pip install kojo-fan-art
(work) % python -q
>>> import the_solitary_castle_in_the_mirror
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'the_solitary_castle_in_the_mirror'

(uvの仮想環境を有効にして)pip installしたのにimportできませんでした。

実は、uvの仮想環境にはpipがない5んですね6

(work) % python -m pip list
/.../.venv/bin/python: No module named pip

なので、pip install kojo-fan-artしたpipは、仮想環境の外のpipなのです!

(work) % which pip
/.../.pyenv/shims/pip

現在有効にしているuv製仮想環境にインストールできていなかったので、importできなかったわけです。
学んだのは、uvコマンドで仮想環境をはじめたら、依存ライブラリのインストールではuvを付け続けるということです

終わりに

uvをvenv(やvirtualenv)同様の仮想環境と考えたために小さく躓いた点を記録しました。
uvを使うときは、venvなどのことは一度忘れuv venvuv pip installを使っていくことになるのですね。

uvのソースコードをちょっと覗いた感じ、Pythonの仮想環境やpipの動きをRustで再実装している印象を受けました(uv venv7uv pip install8)。
Python実装のツールを使わずにRustに置き換えているから速いという理解です。

一方、再実装の中では完全な互換性をもたせない方針9のようで、今回経験したようにPythonの仮想環境の知識をそのまま適用できない点もあるようです(ゼロリセット!)。
uv pipは、pipそのものやそのラッパーではないのだと思います。
pipと近い振る舞いをするように実装されているだけで、uvの仮想環境にpipはありません。


  1. 読んで気になった点のアウトプット
  2. brew install uv https://github.com/astral-sh/uv/tree/0.1.31?tab=readme-ov-file#getting-started
  3. Virtual environments by default https://github.com/astral-sh/uv/blob/0.1.31/PIP_COMPATIBILITY.md#virtual-environments-by-default
  4. 奇しくも私も.venvという名前に行き着きました
  5. venvでも、できらあ!
  6. uv venv --seedで「Install seed packages (pip, setuptools, and wheel) into the virtual environment」とのことです。でもどんなユースケースで使うんだろ?
  7. ここから https://github.com/astral-sh/uv/blob/0.1.31/crates/uv/src/commands/venv.rs
  8. ここから https://github.com/astral-sh/uv/blob/0.1.31/crates/uv/src/commands/pip_install.rs
  9. 途中でも引いた https://github.com/astral-sh/uv/blob/0.1.31/PIP_COMPATIBILITY.md 参照。私の気持ちも置いときます

F#に慣れるために、自分だけのクソコンソールアプリケーションを作る

はじめに

からあげ帝国、バンザイ!! nikkieです。

春ですね。F#する春です🌸

慣れるための素振りをしていきましょう。

目次

F#で自分だけのクソコンソールアプリケーションを作る

この記事では、F#で小さなコンソールアプリケーションを作ります。
我が主(=皇帝)のからあげさんの記事に、オマージュ捧げていく形となります。

Python1

.
├── unko/
│   ├── __init__.py
│   └── unko.py
└── pyproject.toml

F#版(今回作ったもの)

.
├── Unko/
│   └── Unko.fs
├── Program.fs
└── UnkoFSharp.fsproj
% dotnet run
puripuri

順を追って見ていきましょう。

コンソールアプリケーションを作る

dotnet new console -lang F#コマンドでスキャフォールディングします。

このドキュメントとは異なり、Project.fsには

printfn "Hello from F#"

としか書かれていません。
これをドキュメントにあるようにしていきます。

let deru = printfn "puripuri"

[<EntryPoint>]
let main argv =
    deru
    0

deru関数を定義し、main関数の中で呼び出しています。

ちなみにコンソールアプリケーションはこちら

「明示的なエントリ ポイント」を使ってますね

モジュールを導入

このあたりはGPT-4に聞きつつ、ドキュメントにあたって進めました。

1つのファイルにderu関数があるのはクソの系譜の美学に反するので、別のファイルに分離していきます。
これは「モジュール」というそう2

ルート直下のUnko.fsに切り出してもよいのですが、ディレクトリを導入することにしました(Unko/Unko.fs)。

module Unko

let deru = printfn "puripuri"

fsprojファイルに追加します。

  <ItemGroup>
+    <Compile Include="Unko/Unko.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

ここは順番に意味があるそう。
逆順だとビルドできません。

Program.fsではopenします。

+open Unko
-let deru = printfn "puripuri"

[<EntryPoint>]
let main argv =
    deru
    0

頻繁に使用されるモジュールと名前空間に open キーワードを使用すると、そのモジュールまたは名前空間のメンバーを参照するときに、完全修飾名の代わりに短い形式の名前を使用することができます。

Unkoモジュールのderu関数は、Unkoモジュールをopenするとderuとして参照できるようです

こうしてF#版Unkoが完成しました!🙌
Unko/Unko.fsの文字列を変更してdotnet runすると、出力される文字列が変わります。

終わりに

F#に慣れるために、小さなコンソールアプリケーションを作る素振りをしました。
モジュールを作り、それをopenするんですね(open Unko

fsprojファイルはファイルごとにCompileを書く必要があるようです。
ディレクトリを指定できたら楽な気がするんですが、F#の大きなライブラリはどうしてるんでしょうか3

P.S. nikkieによるunkoオマージュギャラリー

皇帝のお言葉


  1. pyproject.tomlを使ったバージョンで示します(P.S.参照)
  2. 名前空間というやつでもできるかも(宿題事項)
  3. VS Code拡張のIonideに案内された fantomas のリポジトリを見ると、なにかわかるかも

pip installのオプションに--no-cacheなんてあったっけ? --no-cache-dirを短縮できるのか!!

はじめに

3月のPython Monthly Topicsを読んでいたnikkie氏。

パッケージのインストールpip installuv pip installの処理時間を比較している。あれ、--no-cache?」

$ time pip install pandas --no-cache

$ time uv pip install pandas --no-cache

pip installに、私の知らないオプションがある...!」(※--no-cacheのこと)

目次

pip installのオプションに--no-cacheなんてあったっけ?

ドキュメント

pip installのドキュメントを見てみましょう。
https://pip.pypa.io/en/stable/cli/pip_install/#options

--no-で始まるオプションはいくつかありますが、--no-cacheはヒットしません。

コマンドライン--helpを渡します。
pip install --helpでもpip --helpでも「General Options」として--no-cache-dirが出てきます

  --no-cache-dir              Disable the cache.

pipのドキュメントではここにあります1
https://pip.pypa.io/en/stable/cli/pip/#general-options

ヘルプメッセージやドキュメントには--no-cacheというオプションは記載がないのですが、Python Monthly Topicsのpip install --no-cacheというコマンドは手元で動作しています
なぜなんでしょう?

実装

pipのリポジトリを検索して見つけたのがこちら。
https://github.com/pypa/pip/blob/24.0/src/pip/_internal/cli/cmdoptions.py#L717-L724

no_cache: Callable[..., Option] = partial(
    Option,
    "--no-cache-dir",
    dest="cache_dir",
    action="callback",
    callback=_handle_no_cache_dir,
    help="Disable the cache.",
)

この箇所は、functools.partialを使って、optparse.Optionの一部の引数を部分適用しています。

--no-cacheというオプションはなさそうでした。

なぜ--no-cacheと指定できるかを考えていて思い出したのは、argparseの例。
https://docs.python.org/ja/3/library/argparse.html#prefix-matching

parse_args() メソッドは、デフォルトで、長いオプションに曖昧さがない (先頭文字列が一意である) かぎり、先頭文字列に短縮して指定できます:

-baconオプションを-bacと指定する例があります。

pipの実装に使われているoptparseは

バージョン 3.2 で非推奨: optparse モジュールは廃止予定であり、これ以上の開発は行われません。argparse モジュールを使用してください。

という存在ですが、optparseでも引数の短縮形はサポートされている記載がありました!
https://docs.python.org/ja/3/library/optparse.html#how-callbacks-are-called
opt_strのところ。

例えば、ユーザが --foobar の短縮形として --foo をコマンドラインに入力した時には、opt_str は "--foobar" となります。

どうやらpipの--no-cache-dirオプションを短縮して、--no-cacheと書いていた(書けていた)のではないかと思われます。

--no-cache-dirを短縮できることを検証

optparseで検証プログラムを書いてみます2

>>> options  # --no-cache-dirと指定
<Values at 0x102402550: {'no_cache_dir': True}>
>>> options2  # --no-cacheと短縮して指定
<Values at 0x102402790: {'no_cache_dir': True}>

短縮して指定できることが確認できました。

終わりに

pip install --no-cacheは私の知らないオプションではありませんでした。
--no-cache-dirの短縮で、optparseやargparseでサポートされています(標準ライブラリ以外に関しては未調査です)。

短縮できる便利さはありつつも、個人的には--no-cache-dirと短縮せずに使っていきたいですね。
--no-cacheとも--no-cache-dirとも指定できるのはなぜ?」と混乱の元になりますし、--no-cacheの情報はググラビリティが低めでした。
The Zen of Pythonにも「Explicit is better than implicit.」とありますし。

P.S. uv pip installでは?

% uv --version
uv 0.1.31

uv --helpと打つと

  -n, --no-cache
          Avoid reading from or writing to the cache

おそらくここ
https://github.com/astral-sh/uv/blob/0.1.31/crates/uv-cache/src/cli.rs#L10-L28

alias = "no-cache-dir"とあるので、--no-cache--no-cache-dir(の2つ)をサポートしていそうです。

% uv pip install pandas --no-cache-dir

pipの実装は短縮ができるので、--no-cache-dもいけます(実際にやるかはおいておいて)。
ところがuvは上記の二者択一です。--no-cache-dはサポートされません。

% uv pip install pandas --no-cache-d
error: unexpected argument '--no-cache-d' found

  tip: a similar argument exists: '--no-cache-dir'

めちゃめちゃ重箱の隅ですけど、uvのオプションの指定の実装はpipと完全互換とは言えないのですね。


  1. 直リンク https://pip.pypa.io/en/stable/cli/pip/#cmdoption-no-cache-dir
  2. parse_argsにはデフォルトでsys.argv[1:]が渡ります https://docs.python.org/ja/3/library/optparse.html#optparse.OptionParser.parse_argsコマンドラインで何回も実行して検証するのは少し手間なので、プログラム中で引数を渡すことにしました

素振りログ:kfpのDSLで書いたパイプラインを、Kubeflow PipelinesでもVertex AI Pipelinesでも実行する

はじめに

ナイスゲーム!!👏👏👏 nikkieです。

先日、小さな気づきを記事にしました。

以下の宿題に取り組みます。

過去記事に沿ったYAMLファイルを当初Vertex AI Pipelinesにアップロードしたのですが、これは弾かれてしまいます。

小さな(練習用)パイプラインをkfp 2系でKubeflow PipelinesでもVertex AI Pipelinesでも動かします

目次

kfpで書いたパイプライン

こちらの記事をもとにしました。

わかったこと

  • componentにするPythonの関数には型ヒントが必要
    • ただジェネリック型のlist[]表記は現時点で非対応。listlist[Any]の意なので書きたくなく、typing.Listを使った
  • 参考記事で使っていたkfp.components.func_to_container_opは2.7.0にはなかった
    • 参考記事はcomponentの定義と、pipelineの定義を分けている(componentのYAMLとpipelineのYAMLができるっぽい)
    • 上で示したやり方は、パイプラインとして動かすDockerイメージにkfpがインストールされる(必要なのかもしれないが、関数の中身が依存しているものではないから除けないかと考えている)

ローカルのKubeflow Pipelinesにて

過去にKubeflow Pipelines 2.0.3・kfp v1で実行していました。

今回の差分

  • Kubeflow Pipelines 2.0.3 -> 2.1.0
  • Kubernetes環境は、Docker Desktop -> Rancher DesktopKubernetes v1.28.6)
  • DSLコンパイルするkfpも2系に上げています(1.8.22 -> 2.7.01

コマンドを流してKubeflow Pipelinesのリソースができるのを待ちます
https://www.kubeflow.org/docs/components/pipelines/v2/installation/quickstart/
「Deploy a KFP standalone instance into your cluster」参照2

export PIPELINE_VERSION=2.1.0

kubectl apply -k "github.com/kubeflow/pipelines/manifests/kustomize/cluster-scoped-resources?ref=$PIPELINE_VERSION"
kubectl wait --for condition=established --timeout=60s crd/applications.app.k8s.io
kubectl apply -k "github.com/kubeflow/pipelines/manifests/kustomize/env/dev?ref=$PIPELINE_VERSION"

試行錯誤メモ

  • Rancher DesktopのPreferences、Virtual MachineのCPUとMemoryは少なめなので(少し)増やす必要があった3
  • controller-managerがOOMKilledされていた
    • kubectl editしてmemoryのlimitsを30Miから引き上げた(動いているのをtopしたところ70Mi使っているので、次回は100Miくらいにすればよさそう)
  • proxy-agentが起動しないのは、諦めた
    • ログを見ると、curl http://metadata.google.internal/computeMetadata/v1/instance/zoneしていて「Could not resolve host: metadata.google.internal」で落ちてしまう

port-forwardしてWeb UIは動きました。
手元でcompileしたYAMLファイルをuploadします。

GCPのVertex AI Pipelinesにて

こちらにも手元でcompileしたYAMLファイルをuploadします。

終わりに

pipeline_rootや永続化という宿題は残っていますが、kfp(v2)が提供するDSL機械学習パイプラインを定義し、それをKubeflow PipelinesでもVertex AI Pipelinesでも実行できることを体験しました。

Vertex AI Pipelines、各社テックブログでもたびたび見かけるのですが、Kubeflow Pipelinesの経験から全然知らない技術ではないと言えたので、記事を読んでいけそうと思えました。


  1. 先日の気づきの記事で使ったのと同じバージョンです
  2. 最後のコマンドだけがv1のときと違いました(「/env/dev」ではなく「/env/platform-agnostic-pns」)。もしかすると、立ち上がらなかったリソースと関係あるかもしれません
  3. 私と同じM1 Macで環境構築した記事でもオススメされてました。「I also recommend you to adjust the # CPUs and the Memory (GB) in Preferences > Virtual Machine to avoid Out of Memory errors (OOM).

macOSにF#の環境を構築する

はじめに

シャーッ! nikkieです。

春ですね(* ̄ω ̄*)🌸1
ということで、環境構築していきましょう!

目次

F#

F# は、簡潔性、堅牢性、パフォーマンスの高いコードを書くためのユニバーサル プログラミング言語です。

OCamlから多くの要素を引き継いだ関数型とオブジェクト指向のマルチパラダイムである。
型安全であり、型推論の機能をもつ。

F#の開発環境

macOSで環境構築

.NET SDKインストーラをダウンロードし、それを実行するだけと、めちゃ簡単です。

% dotnet --version
8.0.204

dotnet fsi対話モードとなります

> printfn "Hello World!!";;
Hello World!!
val it: unit = ()

VS Codeにも拡張を入れました。

終わりに

F#、初めての概念が多く言語としては難しいと感じますが、環境構築は詰まることなく行きました!
F#のコーディングは痛気持ちいい体験ですね3

なぜ突然のF#?
こちらをどうぞ

まだまだ上手くなれそうでとってもワクワクです


  1. NEWS|TVアニメ「サクラクエスト」公式
  2. 別記事より
  3. ゴミ捨て場の決戦を観に行ったら、谷地ちゃんも言ってました

Weights & Biases(WandB)ってどんなツール? データを記録だけしてみる第一歩

はじめに

わんだほー!
きゃー!〜〜!!💚 nikkieです

Weights & Biases(ウェイツ・アンド・バイアスィズ)、触ってみました。

目次

技術書典15『WandBで始める実験管理』

LLMに関するMeetupのアーカイブが多くあって知っていたWeights & Biases1
https://www.youtube.com/@WeightsBiasesJapan

アーカイブではLLM(や機械学習モデル)の訓練で便利と聞きましたが、どんなツールかは触って理解しているわけではありませんでした。

また、こちらの技術同人誌を積ん読していました。

リンク集リポジトリがこちらです。

いまこそ紐解くとき!

ランダムな数値をWandBに記録してみる

第2章「WandB 入門」の最初のコードを素振りします(2.1)。
機械学習モデルは訓練せずに、ランダムな数値をWandBに記録だけします。
WandBとは何かを掴むのにうってつけの例だと思いました

  • Python 3.11.8
  • wandb 0.16.6
    • アカウントも開設しています
    • https://www.wandb.jp/ からサインアップ。この記事の範囲は無料です2

環境変数WANDB_API_KEYを設定しました。
API KeyはUser settingsの中にあります(Danger Zone)

WandBにbasic-introプロジェクトを作って、3回のRunを記録するコードを書いています。
1回あたりの記録のコードは以下です。

with wandb.init(
    project="basic-intro",
    name=f"experiment_{run}",  # 例: experiment_0
    config={
        "learning_rate": 0.02,  # この例ではダミーの値
        "architecture": "CNN",
        "dataset": "CIFAR-100",
        "epochs": 10,
    },
) as run:

    epochs = 10
    offset = random.random() / 5
    for epoch in range(2, epochs):  # 8回ランダムな数値を生成
        loss = 2**-epoch + random.random() / epoch + offset
        acc = 1 - loss
        run.log({"acc": acc, "loss": loss})

スクリプトを実行すると、Runが3回記録されます。
ローカルのwandbディレクトリに記録してから、WandBのWebアプリ側にも記録されるようです。

ブラウザで見た画面です(Runsを見ています)

グラフも見られます

感想(終わりにに代えて)

ちょっとしか触っていないので誤解しているかもしれませんが、「MLflow3と似ているな」という印象です。

  • 両ツールとも、モデルに関するデータ(例:訓練中のロスの値)をローカルに保存
  • WandBはローカルだけでなくWebアプリにも保存。ダッシュボードとして見られる
    • 他方MLflowも、ローカルでダッシュボードを立てられます。またDatabricks社が提供するサービス版があるように記憶しています

モデルに関するデータを記録する機能について細部を見れば違いがあるかと思いますが、両ツールとも機械学習まわり(特に実験管理)のペインを解消するツールなのだと思います。
今回取り上げた技術同人誌では、WandBのいろいろな事例が掲載されていたので、もうちょっと触ってみたいところです。


  1. 「LLMの評価方法」を視聴して書いたブログ
  2. パーソナルアカウントの範囲は無料です。チームで共有する機能は有料です https://www.wandb.jp/pricing
  3. 先日触りました