絞り込み

最新の投稿

NoImage Polygon
プログラミング

Nullはなぜ危険なのか

こんばんは、オキリョウです Kotlin 1.7.0がついにきましたね! https://blog.jetbrains.com/kotlin/2022/05/kotlin-1-7-0-beta/ みなさん期待を膨らませていることだと思います。 ところで、このバージョンの目玉として取り上げられているものの中に、max関数及びmin関数があります。 名前を見てわかる通り、これはリストの中の最大、最小の値を取ってくる関数になります。 こんな原始的な機能すらKotlinにはなかった・・・というわけではありません。 代わりにmaxOrNull、minOrNullという関数が存在します。 では何が違うのかというと、最大値及び最小値が存在しないときの挙動です。従来 -> nullを返す今回 -> エラーを投げるとなっています。 最近ではNullは敬遠されがちです。その流れを汲み取ってこの関数を実装したのでしょう。 ものすごく良さげな機能に見えますね。 でも、実は私、あまり良い気がしていません。 とりあえずNullは危険だ!という理由でより危険なこの関数を使う人がいるかも?と思っているからです。 そこで、なぜNullが危険と謳われているのかを解説していきたいと思います。 Nullの危険性Nullの危険性に触れてみるまず、以下のコードを見てみましょう。こちらはJavaのコードになっています。public static void main(String[] args) { Integer value = 5; System.out.println(value * value); }至って普通のコードですね。出力結果は以下のようになります。25 それではこのコードを少し編集してみましょう。public static void main(String[] args) { Integer value = null; System.out.println(value * value); }value変数に入れる値をnullにしてみました。この場合どのような挙動をすると思いますか? そもそも数値型にnullなんか入らなさそうだからコンパイルすら通らなさそうです。 実はこれ、コンパイルが通るんです。 ただ、実行しようとするとエラーを吐いて止まります。Exception in thread "main" java.lang.NullPointerException Nullの正体なぜ上記のような挙動をするのでしょうか? それはnullが何かを理解すればすぐにわかります。 nullは何もないことを表す番地へのポインタです。 先程の実行例を見てみましょう。Integer value = 5;これは、5という値がとあるアドレスに格納されていて、そのアドレスの番地をvalueが保存している・・・という構造になっています。 ではnullの場合はどうでしょう?Integer value = null;これは、何も意味がないというアドレス(通常は0アドレスとからしい)の番地をvalueが保存しています。 ここから、value自身は両者とも同じようにポインタを指していることがわかります。 問題は、そのポインタの先に値がある場合とない場合があるということです。 そのせいで、実行時ではないとエラーがわからないのです。 説明がややこしくてわからん!という人は以下のように考えましょう。 Integer型とは、暗黙的にInteger or null型になっていた これらはもちろん、他の型でも当てはまります。 例外として、プリミティブ型と呼ばれるポインタを使わない型にはnullは使えません。 何が恐ろしいかここまで色々話してきましたが、結局nullの何が恐ろしいのでしょうか? まとめると以下の通りです。実行するまでエラーが出ないため、本番環境で爆発する可能性があるnullを参照した場所でエラーが起きるため、どこでnullが代入されたか(すなわち原因)を特定するのは困難nullを使わないように制御する方法がないため、人力でnullチェックをする必要がある(人力なので忘れがち)これらのことから、エンジニア界隈ではnullを恐れる人が多発する事になりました。 このことから、nullを開発した張本人も後悔の念を表明しています。 https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ Nullの現在ここまででnullの恐ろしさを十分に理解できたのではないでしょうか? しかし、ここまで恐ろしいと言われているnullをそのまま放置する人類ではありません。 比較的最近作成された言語では十分な対策が取られています。 有名な言語としてKotlinが挙げられます。 ではどのような対策をしているのでしょうか? やっていることは単純です。null非許容型を用意したんです。 実際のコードを見てみましょうval value: Int = null // NGこのように、普通の型にはnullが入らないようにしました nullを使いたい場合は、下のように?をつけることで許容型にする必要がありますval value: Int? = null // OKまた、この?を外さない限り、Int型のメソッドは呼び出せません。 外すにはnullが変数の中に入っていないか(いわゆるnullチェック)をする必要があります。 こうすることで強制的にnullチェックをさせるようにしました。 これで Int = Int or nullみたいなことは無くなったわけです。 もちろん、nullチェックがしやすい構文も用意されており、nullとの付き合いが断然楽になりました。 まとめかつて、nullは非常に恐ろしいものでした。 最も簡単にエラーを引き起こしてしまいますし、かつ原因がわかりづらい。 しかし、現在ではコードによる強制チェックができるようになったため、このようなエラーは随分と減りました。 むしろ、確実にチェックしなくてはならない存在となったのです。 チェックしないとそもそも関数呼び出せないですからね。 そのため、現在のコード上でのnullは結構使いやすい存在になったと個人的には思っています。 かといって乱用するのもどうかとは思いますが・・・ 昔よりはnullを使うデメリットがガッツリ下がったと思ってください。 もちろん、Null非許容型が用意されていない言語の場合、nullは非常に危険です。 例えばJava, PHP等の古い言語、またモダン言語でも昔のDart等が挙げられます。 できる限り使わないようにするか、Option型の導入を検討しましょう。 それでは、お疲れ様でした! 補足冒頭で述べた「nullより例外の方が危険だ」という事について補足しておきます。 例外の何が恐ろしいのかというと、コンパイラによってチェックされない事です。 どこからでも好きに投げれて、try-catchを使ってチェックをしなくてもコードに怒られない・・・ そうですよね? ちょっと怖くないですか? 先程のnullで説明したことと同じような課題が出てきています。 このことから「近代のgoto文だ」と批判する人すらいるほどです。 私もこれには同感です。そこらでポンポンエラーを投げるくらいならnullで表現する方が安全だと思います。 大概のモダン言語上のnullであれば確実にチェックをする必要がありますので想像以上に安心して動かせます。 値が存在しなかったからnullを返すわけですし、結構自然な気もしますしね。 いや、nullじゃ表現力が足りない。型の堅牢さも足りていないから例外は必須だ!という人。良い着眼点です。 そんな人はぜひResult型について勉強してみてください。これを使えば完全に例外のない世界も実現可能です。

> 内容を見る

プログラミング Polygon
プログラミング

MacのChromeで学校用プロフィールのショートカットを作る

Chromeのプロフィール選択で、Windowsならデスクトップショートカットを作るボタンがあるんですけど、Macにはないので自作します。 時々プロフィール選べない問題 皆さんChromeで「前回開いていたページを開く」にしていますか?賛否あると思うんですが、基本的にはこれが一番便利だと思います。 もちろんこの場合、学校用プロフィールを作っておかないと事故ります。 つまり、「前回開いていたページを開く」人は、「起動時に(プロフィール選択を)表示する」にチェックを入れないとやばいです。 しかしですね、Macだけかもしれませんが、Chromeがプロフィール選択画面をすっ飛ばす謎の現象が多々発生するんですよ。 講義室で開く用のショートカットを作る ということで、「最初から学校用のプロフィールが開くショートカット」を作ります。 https://superuser.com/questions/377186/how-do-i-start-chrome-using-a-specified-user-profile Super UserのLinuxオタクによると、Application Supportにプロフィールがおいてあるらしいですよ。それで--profile-directory を付ければいいらしいです。 デフォルトの写真を確認$ qlmanage -p ~/Library/Application\ Support/Google/Chrome/Default/Google\ Profile\ Picture.pngまずDefault のプロフィール写真を見ましょう。ほとんどの場合、個人用Googleアカウントの写真が出るはずです。この写真が学校用だったら次の手順は飛ばしてください。 余談ですがqlmanageで立ち上がるプレビューは「プレビュー.app」ではないのでcontrol+Cで死にます。ちょっと確認したいときはこっちを使いましょう。(参考) $ PROFILE_NAME=$(ls ~/Library/Application\ Support/Google/Chrome | grep ^Profile | head -n 1) $ echo $PROFILE_NAME # Profile 1複数のプロフィールを作ると、Application SupportにProfile nというディレクトリができます。Default以外は学校用プロフィールしか作ってない場合、Profile 1 が学校用になるはずです。 $ cat <<EOT > ~/Desktop/学校用Chrome.sh #!/bin/bash open -b com.google.Chrome -n --args --profile-directory="$PROFILE_NAME" EOT $ chmod +x ~/Desktop/学校用Chrome.shスクリプトを作ります。エディタ宗教戦争に「エディタを使わない」という手段で抵抗。あとmacでしか使わないからbashでいいでしょ別に。 -b でappファイルの名前でなくバンドル識別子を使います。こっちのほうが確実。あと-n でアプリを無理やり増やします。これを付けないとすでに起動してるときに動作しない。(参考) はいこれで学校用ショートカットができました。 ターミナル.appの挙動について 「正常に終了したら閉じる」を選んどけば、勝手にexit; が入って閉じられるようです。

> 内容を見る

プログラミング Polygon
プログラミング

4200000000Hzでゴリ押す!!!(2)

今回はアルゴリズムとCPUのパワーでパズルを全力で解きます。 Exponential Idle(Android版 / iOS版)というクリッカー系放置ゲーム中のミニゲームである、矢印パズルを攻略します。 こちらのnoteの記事にめっちゃ影響を受けて書くことにしました。 と言っても、初級と中級は私でも頑張れば解けたのと、上級も先行研究があるので、エキスパートに絞って話を進めていきます(まあ上級も同じプログラムをちょちょっといじれば簡単に解けますが)。訂正とお詫び前回のサムネイルの一部に「♪ラデツキー行進曲(1.5倍速)」とありましたが、正しくは1.543倍速です。謹んでお詫び申し上げます。ルール説明(エキスパート)図1:矢印パズル(エキスパート)のルール六角格子状に並べられたタイルをタップすると、そのタイルを含む周囲(最大)7個のタイルがそれぞれ時計回りに60°回転します。 それを繰り返してすべてのタイルの向きを上向きに揃える、というルールです。目標盤面を入力として受け取り、各タイルのタップ回数を出力する。基本の考え方1回タップすることに60°回転するので、6回同じ場所をタップすれば盤面の状態は変わらず、加法巡回群をなすような6元集合、$\mathbb{Z}/6\mathbb{Z}$ を持ってくる。解法案1参考文献[3]と同様に、$\mathbb{Z}/6\mathbb{Z}$ 上の37次の線形方程式を解くことに帰着させる方法です。が。 聡明な読者の方ならお気づきでしょう。$\mathbb{Z}/6\mathbb{Z}$ は体でしょうか。2,3,4は乗法逆元を持たないので体ではないですね。 すると線形方程式を解くときに除法が使えないということになり、掃き出し法は使えないことになります(ピボットを1にできないだけで掃き出せないわけではないだろうが、一般的なアルゴリズムをそのまま持ってくるのは厳しそう)。2掃き出しで解けない(難しい)と言っても、元は有限個しかないので $6^{37}=6.2 \times 10^{28}$ 通りすべてを試してしまえという考え方。猿。 いや無理やけど。もし1パターンを1nsで検証できたとしても6.2e19 s掛かります。実際は1パターンの検証で約7x37回の足し算をすることになるのでもっとかかります。 imos法で最適化すればどうにかなるようなレベルの話ではないです。やるだけ無駄。3大本命。DFSです。DFS自体は木の全探索なのですが、探索の必要がない部分を枝刈りすればだいぶ計算量が減ります。どれぐらい絞れるかは後述。 今回はこれを採用。プログラムこれが解を探索するプログラムです。 入力の数字は、ディスプレイモードを数字にして表示された数字ということになっています(内部的には-1して持っているが)。#include <iostream> #include <vector> #include <tuple> #include <algorithm> using index_t = std::tuple<int, int>; using board_t = std::vector<std::vector<int>>; const int mod = 6; const size_t board_row = 7, board_col = 7; void dfs(board_t &board, board_t &hands, index_t index); void flip_around(board_t &board, index_t index, int amount); index_t next(index_t index); bool is_in_board(index_t index); void print_board(const board_t board); const index_t end_index = next({ 6, 6 }); int main() {   board_t board(board_row, std::vector<int>(board_col));   board_t hands(board_row, std::vector<int>(board_col));   for (index_t i = { 0, 0 }; i != end_index; i = next(i)) {     const auto [r, c] = i;     std::cout << r << " " << c << ": " << std::flush;     std::cin >> board[r][c];     board[r][c]--;   }   dfs(board, hands, { 0, 0 });   return 0; } void dfs(board_t &board, board_t &hands, index_t index) {   if (index == end_index) {     print_board(hands);     exit(0);     return;   }   auto [r, c] = index;   // 右端辺での枝刈り   if (c == 6 && r > 3 && board[r - 1][c - 1] != board[r - 1][c]) {     return;   }   // 下端辺での枝刈り   if (r == 6 && c > 3 && board[r - 1][c - 1] != board[r][c - 1]) {     return;   }   // 上端、左端辺以外での枝刈り   if (r != 0 && c != 0) {     // 左上マスを0にするように動かす     const int hand = (mod - board[r - 1][c - 1]) % mod;     hands[r][c] = hand;     flip_around(board, index, hand);     dfs(board, hands, next(index));     flip_around(board, index, mod - hand);   } else {     for (int hand = 0; hand < mod; hand++) {       hands[r][c] = hand;       flip_around(board, index, hand);       dfs(board, hands, next(index));       flip_around(board, index, mod - hand);     }   } } void flip_around(board_t &board, index_t index, int amount) {   // 正体不明だがちゃんと最初だけ初期化処理が走る   static std::array<index_t, 7u> dpos{     dpos[0] = { -1, -1 },     dpos[1] = { -1, 0 },     dpos[2] = { 0, -1 },     dpos[3] = { 0, 0 },     dpos[4] = { 0, 1 },     dpos[5] = { 1, 0 },     dpos[6] = { 1, 1 }   };   const auto [r, c] = index;   for (const auto [dr, dc] : dpos) {     if (!is_in_board({ r + dr, c + dc })) continue;     board[r + dr][c + dc] += amount;     board[r + dr][c + dc] %= mod;   } } index_t next(index_t index) {   auto [r, c] = index;   if (c == 6 || c - r >= 3) {     r++;     c = std::max(r - 3, 0);   } else {     c++;   }   return { r, c }; } bool is_in_board(index_t index) {   auto [r, c] = index;   return std::abs(r - 3) <= 3 && std::abs(c - 3) <= 3 && std::abs(c - r) <= 3; } void print_board(const board_t board) {   for (size_t i = 0; i < board_row; i++) {     for (size_t j = 0; j < board_col; j++) {       if (is_in_board({ i, j })) {         std::cout << board[i][j] << " ";       } else {         std::cout << "  ";       }     }     std::cout << "\n";   }   std::cout << std::endl; } こんなに const とか & とか書くんだったら最初からRustでやればよかったなと若干の後悔。 ぼちぼち解説していきます。 まずここ。using index_t = std::tuple<int, int>; using board_t = std::vector<std::vector<int>>;index_t というエイリアスに std::tuple<int, int> を当てています。 index_t index = {row, column}; となっているならば、index は、row 行 column 列のパネルの位置を表します。 board_t には std::vector<std::vector<int>> を当てていますが、 board_t board(R, std::vector<int>(C)); としたならば、board は、R 行 C 列のパネルの二次元配列を表します。(入力と)パネルの現状態と出力する手数に使います。 図2:indexの指し方 次はここ。index_t next(index_t index) {   auto [r, c] = index;   if (c == 6 || c - r >= 3) {     r++;     c = std::max(r - 3, 0);   } else {     c++;   }   return { r, c }; }next関数はその名の通り、次に探索する(タップ回数を決定する)点を決定する関数で、DFSでの探索順に直接関わってきます。 定義はまあ見りゃわかりますね(ifの条件に c - r >= 3 とか書いてあるが、これは条件をいっぱい書きたくなかったからうまいこと纏めてあるだけ)。 2行目の auto [r, c] = index; ってのは構造化束縛というC++17以降で追加された機能です。分割代入だと思って大体問題ありません。 一応探索順を図に示しておくとこうです。図3:DFSの探索順この順序にしているのは、単純に分かりやすいという以外にもう一点理由があって、枝刈りがしやすいという点。 枝刈りの方法はdfs関数を見ればわかります。 void dfs(board_t &board, board_t &hands, index_t index) {   if (index == end_index) {     print_board(hands);     exit(0);     return;   }   auto [r, c] = index;   // 右端辺での枝刈り   if (c == 6 && r > 3 && board[r - 1][c - 1] != board[r - 1][c]) {     return;   }   // 下端辺での枝刈り   if (r == 6 && c > 3 && board[r - 1][c - 1] != board[r][c - 1]) {     return;   }   // 上端、左端辺以外での枝刈り   if (r != 0 && c != 0) {     // 左上マスを0にするように動かす     const int hand = (mod - board[r - 1][c - 1]) % mod;     hands[r][c] = hand;     flip_around(board, index, hand);     dfs(board, hands, next(index));     flip_around(board, index, mod - hand);   } else {     for (int hand = 0; hand < mod; hand++) {       hands[r][c] = hand;       flip_around(board, index, hand);       dfs(board, hands, next(index));       flip_around(board, index, mod - hand);     }   } }私もそこまでアホではないし、この関数は深い再帰を伴うので引数の大部分は参照で受けるようにしました。 部のC#erの皆さんが卒倒するかもしれませんが、vector<T>は値型です。というか、C/C++に値型・参照型という分け方はありません。 明示しない限り常にすべて値渡しになるので、ちゃんと参照渡しになるよう指定してやります。 さておき、枝刈りの話。これも図に示したほうが早いでしょう。図4:dfs関数の枝刈り図3に示した探索順から分かるように、27,32,36のノードでは、そのノードを操作した後は左上と真上の変化がないので、その2箇所の状態は必ず同じでなければならず、そうでなければ後退して探索し直すことになります。34,35,36でも左上と左下で同じことが起こります。 また図3から分かるように、ある時点で探索しているパネルの左上にパネルがあったとき、探索しているパネルを動かした後に再び左上のパネルが変わることはないので、左上は0にならなくてはなりません。よって左上のパネルを元にただ一通りに決まります。逆に言えば、0~5の中から自由に選べるのは左上にパネルが存在しない0,1,2,3,4,9,15のたった7個のパネルのタップ回数のみです。したがって、検証の必要なパターン数は $6^7 \approx 10^{5.45}$ 通りだけになります(ただし、関数呼び出し自体が重いことと、これらの点の不正性が分かるまでに再帰の深いところに潜ることがあるのでやはりある程度計算に時間はかかる)。 こんな感じで殆どの点で枝刈りが行えることがわかりますね。 if (index == end_index) {   print_board(hands);   exit(0);   return; }で、最後36番が終わればここの分岐にたどり着いて終わりです(なんでこの人exitとreturn両方書いてるんだろう)。 でも図4を見てもらえばわかりますが、36番ノードだけチェックが入ってないんですよね。 これでいいのかと言われれば良くて、解が存在するならば、36番ノードだけが非0であることはありません。 流石に解が存在しない問題は生成されないだろうということでここは一つ。 flip_around(board, index, hand); dfs(board, hands, next(index)); flip_around(board, index, mod - hand);ところでこの部分ですが、再帰DFSにおける常套手段です。 ある変換(ここではある1パネルをn回タップすること)が可逆であるならば、dfs呼び出しから帰ってきた後に逆変換(ここでは同じパネルを6-n回タップすること)を行えばうまいことDFSになります。是非覚えておいてください。 以上。閑話void flip_around(board_t &board, index_t index, int amount) {   // 正体不明だがちゃんと最初だけ初期化処理が走る   static std::array<index_t, 7u> dpos{     dpos[0] = { -1, -1 },     dpos[1] = { -1, 0 },     dpos[2] = { 0, -1 },     dpos[3] = { 0, 0 },     dpos[4] = { 0, 1 },     dpos[5] = { 1, 0 },     dpos[6] = { 1, 1 }   };   ... }この部分、何ですか。自分で書いたけどどういう理屈で初期化されているのかわかりません。C++プロの方、教えて下さい。 aggregateならメンバ名を指定して初期化する方法があるけど、その延長と考えていいんですかね。参考文献[1]Conic Games「Exponential Idle - Google Play のアプリ」<https://play.google.com/store/apps/details?id=com.conicgames.exponentialidle> [2]Gilles-Philippe Paille「「Exponential Idle」をApp Storeで」<https://apps.apple.com/jp/app/exponential-idle/id1538487382> [3]ちゃそ「Exponential Idle #2 矢印パズル攻略法(と二元体GF(2)上の線形方程式について)」<https://note.com/so_ra_64/n/n9c2eb6a5ef6f> [4]ますたー。/繰り上げP「焼き甜花ちゃんシリーズの作り方&素材」<https://www.nicovideo.jp/watch/sm37861810> [5]いもす研「いもす法」<https://imoz.jp/algorithms/imos_method.html>

> 内容を見る

プログラミング Polygon
プログラミング

コンピュータ弱者、初めてのシェルスクリプト

恵まれたソフト(illustrator)で初めて作ったカスみたいなサムネそうだ、シェルスクリプト作ろう。きっかけはBlenderのアドオン開発環境を整えようとしたことであった。ただ普段の制作環境とアドオン開発環境を分けたかっただけなのだ。 しかし自分は数行のバッチファイルを書く数分をケチるために最終的に数時間を費やしてしまったのだ。Blenderの設定はどこにある?まず、基本的にWindowsにおいてBlenderの設定ファイルは初回起動時にC:\Users\(ユーザー名)\AppData\Roaming\Blender Foundation\Blender\(バージョン名)に作られる。 詳しい説明は省くが、そこからconfigdatafilescriptsへと派生する。そして次回起動時からそのフォルダにあるファイルの設定を元に新規ファイルが作られる。 今回大事なのはconfigとscriptsである。このフォルダにスタートアップファイル、プリファレンスファイル、プリファレンスでインストールしたアドオン等が含まれているのだ。 単にデスクトップにあるBlenderアイコンをダブルクリックするだけでは、この一つの設定しか使えない。つまり制作環境とアドオン開発環境を分けることができない。 何か方法がないか探してたところ、「環境設定を複数用意する方法・PC/バージョン間で設定を共有する方法【Blender】」という記事を見つけ、方法3.別設定で開く.batファイルを作るを参考にしてバッチファイルを作った。これで一件落着のはずであった。 だが、自分はこう思った。「新しい環境を作るのにいちいちフォルダを作ってちまちまとパスをコピーするのも面倒だし、プログラムを書いてコンピューターに作らせればいいじゃん」 そのせいで苦しむ羽目になったのである。とりあえず書いてみる恥ずかしながら今まで自分はコマンドプロンプトしか使ってなかったので、Windows PowerShellをちゃんと勉強したいという思いがあった(PowerShellのほうがいろいろ出来そうだし)。 そして以下のようなメモをとった。(実際は色々調べながら書いてるので、最初からガチガチに固めて書いたわけじゃない) これを元にPowerShellスクリプトを書いていく。ちなみに自分はVisual Studio Codeを使っている。 まず、スクリプトが引数を受け取るためにはParam() という関数を使えばいいらしいので、Param($name_pref, $name_bat, $version_blender)と書き、blender_add_env.ps1 として保存した。しかしながら、ここで「変数の型どうなってんねん」と感じたので、` $name_pref.Gettype() $name_bat.Gettype() $version_blender.Gettype()と追記し、ターミナルで.\blender_add_env hoge hogehoge 3.0 と実行すると、IsPublic IsSerial Name                                     BaseType -------- -------- ----                                     -------- True     True     String                                   System.Object True     True     String                                   System.Object True     True     Double                                   System.ValueTypeと表示される。$version_belnderがDouble型になってるのがどうも嫌だ。2.93などの中途半端な数字ならまだしも、Blenderのバージョンはもうすぐ3.0になる。このままだと3.0の0の部分は無視されてしまう。それに事故を防ぐために引数は全部String型で受け取りたい。なのでParam(     [string]$name_pref,     [string]$name_bat,     [string]$version_blender )と書き直した。ついでに(個人的に)見やすくした。受け取ったフォルダの名前で現在の階層に新たなフォルダを作る新しいフォルダに移動するconfigとscriptsのフォルダを作るは簡単なので、さっさと調べてNew-Item $name_pref -ItemType Directory Set-Location $name_pref New-Item config, scripts -ItemType Directoryと最初は書いた。しかし「いちいち移動するの無駄じゃん、一行でいけるやろ」と感じたので、New-Item $name_pref/config, $name_pref/scripts -ItemType Directoryと書き直し、さっきのコマンドを再び実行すると、 このように新しいフォルダーがきちんと得られたのでOKである。 次はconfigフォルダとscriptsフォルダの絶対パスを相対パスから得たい。これをするためにはConvert-Pathを使えばいいらしく、$path_config = Convert-Path "$($name_pref)/config" $path_scripts = Convert-Path "$($name_pref)/scripts" $path_config $path_scriptsと書いた。後半の2行は確認用である。例によってあのコマンドを実行すると、 となり、ちゃんと絶対パスが得られることがわかった。 とりあえず空のバッチファイルをNew-Item で作る。$bat_file = New-Item "$($name_bat).bat"そしてAdd-Contentでバッチファイルに追記していく。Add-Content $($bat_file) "set BLENDER_USER_CONFIG=$($path_config)" Add-Content $($bat_file) "set BLENDER_USER_SCRIPTS=$($path_scripts)" Add-Content $($bat_file) "cd C:\Program Files\Blender Foundation\Blender $($version_blender)" Add-Content $($bat_file) "blender.exe"これで終了である。(書くのが面倒くさくて後半投げやりになってしまった)完成だと思った?blender_add_env.ps1だと長ったらしいし文法的におかしい気がするのでaddbleenv.ps1に改名した。 中身はこんな感じである。Param(     [string]$name_pref,     [string]$name_bat,     [string]$version_blender ) New-Item $name_pref/config, $name_pref/scripts -ItemType Directory $path_config = Convert-Path "$($name_pref)/config" $path_scripts = Convert-Path "$($name_pref)/scripts" $bat_file = New-Item "$($name_bat).bat" Add-Content $($bat_file) "set BLENDER_USER_CONFIG=$($path_config)" Add-Content $($bat_file) "set BLENDER_USER_SCRIPTS=$($path_scripts)" Add-Content $($bat_file) "cd C:\Program Files\Blender Foundation\Blender $($version_blender)" Add-Content $($bat_file) "blender.exe"これでやっと終わりだと思った。現実はそうではなかった。 結論から言うと、さらにこのaddbleenv.ps1を実行するバッチファイルを作らなければならなかった。 というわけで、powershell C:\z_ps_script\addbleenv.ps1 %1 %2 %3という内容のaddbleenv.batを書いた。(説明すっとばしますごめんなさい) これをSystem32にぶち込み、エクスプローラーでaddbleenv hoge hogehoge 2.93と実行してみると ! !! !!! !!!! !!!!! で、出来てる~! ~完~ あとがきはじめてじぶんのためにプログラムをかけたのでうれしかったプログラムのかきかたぐだぐだてすともぐだぐだぶんしょうもぐだぐだよくかんがえたらがぞうむだなのある こーどぶろっくでかけやすいこうだるいこんなくだらないプログラムをかくのにふつかもかかったきちょうなさくひんせいさくじいかんをむだにしたけっかんはしらんおまけ

> 内容を見る

プログラミング Polygon
プログラミング

競プロ環境構築#3 Python編

Windowsの方はWSLを入れて読んでください。 Pythonはいいぞ。Pythonはクソ。私がPythonを始めようとしているすべての人(上級者とか逸般人とか言われる人を除く)に言っていることです。 Pythonをやりましょう。 プログラミング初心者が一番最初に触るのに適している言語はPython…そんなわけ無いですね。JavaScriptだと思います(個人の感想です)。 JavaScriptについては別の回でやります。 まあPython人気ですし。やりましょう。図1:Googleトレンド「プログラミング言語」関連トピックの人気順5位がPython(2021年6月28日現在)←執筆をサボってたのがバレちゃう! Pythonが初心者向けである理由ググれ。というのもなんなのでちょっとぐらい僕の見解を話します。動的型付け言語である言語の特性の話ですが、型を意識してプログラムを書くというのは知識なしでは難しいと思います。 単にモノを作りたい、というだけであれば別に静的型付けに拘る必要はないですし、余計な難点が取り払われると考えることができます。 本物の初心者はエラーログを読めないことが多い[独自研究]のでそっちのほうが楽です。実はそんなに新しくないPythonはここ数年で[いつ?]急速に流行りだした言語ではありますが、Python 2.0(廃止済み)が2000年に、Python 3.0が2008年にリリースされています。 …いや言うて新しいやん、Go,Rust,Kotlinあたりと同じぐらいの時期やん、と思われるかもしれません。 が、それは言語としての話であって処理系としての話でいうとPython 3がだいぶ早いです。Go 1は2012年3月28日、Rust 1.0.0は2015年5月16日、Kotlin 1.0.0は2016年2月15日です。 対してCPython v3.0は2008年12月4日です。 まあ大幅な仕様変更が有ったとは言え、随分前からv2.xの処理系は有ったわけですから当然っちゃ当然ですね。 余談ですが、今は見る影もないCPython v1.0.1は1994年2月16日にリリースされました。Gitが生まれたのが2005年といえば相当昔なのがわかると思います。 利用者が多いという点も、情報が多いことに寄与していると思われます。 公式リファレンスも充実しています(もっとも、競プロだから公式リファレンスと多少のモジュールだけ見てれば十分という話ですが)。 競プロにおいてPythonが微妙な点遅い最大の欠点と言っても過言ではない。そう、このCPythonというインタプリタは普通に使うと遅いです。 それはもうとにかく遅い。ABC182-E の言語別実行時間分布 Rust はっや pic.twitter.com/mh6aXMAxNo— scol (@scol_kp) November 9, 2020 上記tweetの一番右が多分CPython 3だと思いますが、AC者滅茶苦茶少ないですよね。なぜかと言えば普通に書くと遅いから。 Python 3自体の使用者はかなり多いのですが(C++に次いで2番目[要出典])、 ユーザーの大半はPyPyというCPythonとは別のインタプリタを使っています。 CPythonで動くコードをほとんどそのまんま動かせて、しかも早いのが素晴らしいところです。 AtCoderで未だにPyPy 7.3.0(Python 3.6)が使われているのが玉に瑕ですが、次の言語アップデートを気長に待ちましょう。Off-side Ruleサッカーの話ではなくて。ここでは、左端から離れる(off-side)規則のことです。インデントでブロックを表します。 対になる概念はCurly-bracket programming(中括弧でブロックを表す)。 Pythonという言語の設計思想は調べてもイマイチわかりませんが、 一つ言えそうなことは「プログラムが動くためには、美しく有るべき(SHOULD)。」ということです。動いてはならない(MUST NOT)、まで強くはないです。 ついでに、現在のPythonの言語仕様は PEP(Python Enhancement Proposal; Pythonをよりよくする提案)という提案を多数取り込むことで出来上がっています。 マージされたPEPの中でも最も有名なのはPEP8でしょう。 Pythonの生みの親であるGuido van Rossumを含む3人によって提案された、コードスタイルについての規則です。 その中では、インデントの推奨サイズ(半角スペース4個、2個でも8個でも、タブスペースでもない)や1行の推奨最大文字数まで指定されています。 それで、それの何が問題かと言えば、コードをコピペしたときにインデントが崩れたとして、それを自動で直すことができません。 コピペライブラリを貼り付ける場合にわざわざインデントを手動で直さなければならず、不便きわまりありません。 中括弧を使わなくともブロック末尾にendキーワードを置くRubyでは問題にならないのですが…… 本編お待たせしました。環境構築について話していきます。 例によって、#1 共通編を前提としますが、 使っているHeadless CMSサービスの都合上、<details> <summary>要素とかidによるページ内リンクが使えずごちゃごちゃしているので、 そのうち大幅改版してDockerfileでも作ることにします。乞うご期待。 正規表現でうまいこと置換すれば任意のHTML要素を置けるとかそういうことを言わない。 さて、Pythonの環境についてですが、一応CPythonとPyPy両方の説明をします。 速度的にはPyPyのほうが手抜きして高速化できるのですが、AtCoderのPyPyではNumPy等の便利なライブラリが使用できません。 CPythonで使えるライブラリはここのQiita記事に書いてありましたが、実際使えそうなのはNumPy、Numba、NetworkXぐらいでしょうか。SciPyも使える時があるのかな。 私は標準ライブラリしか使わないので基本PyPyです。PyPyUbuntuでPyPy3をインストールする方法は複数あります。aptを利用してUbuntu公式リポジトリのpypy3/focal 7.3.1+dfsg-4をインストールする公式で配布されているPyPy3 7.3.0のビルドをダウンロードするpypy.orgからPyPy3 7.3.0のソースをダウンロードしてビルドする手っ取り早い1と2の方法を紹介します(PyPyは公式でも自分でビルドすることは推奨されていないので)。方法1 apt install説明するまでもないような気がしますが。$ sudo apt install pypy3 $ pypy3 -Vはい。方法2 ビルド済みのPyPyをダウンロードこれもtarをダウンロードしてきて展開して適切な場所に配置するだけです。簡単ですね。$ sudo apt install wget $ cd /tmp $ wget https://downloads.python.org/pypy/pypy3.6-v7.3.0-linux64.tar.bz2 $ sudo tar vxjf pypy3.6-v7.3.0-linux64.tar.bz2 -C /usr/local/lib $ cd /usr/local/bin $ sudo ln -s ../lib/pypy3.6-v7.3.0-linux64/bin/pypy3 pypy3 $ pypy3 -VCPythonAtCoderで2021年11月現在採用されているCPythonのバージョンは3.8.2です。aptを利用してUbuntu公式リポジトリのpython3/focal,now 3.8.2-0ubuntu2をインストールするpython.orgからPython 3.8.2のソースをダウンロードしてビルドするとまあPyPyと概ね同じですが、Linux向けビルド済みパッケージを公式が配布していないので、 Ubuntuからの供給が絶たれれば自分でビルドする他なくなります。方法1 apt install$ sudo apt install python3 $ python3 -Vはい。方法2 ソースからビルドする時間がかかるのであんまりしたくありませんが、一応。$ cd /tmp $ sudo apt install wget xz-utils gcc make zlib1g-dev libssl-dev libbz2-dev libffi-dev $ wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tar.xz $ tar vxJf Python-3.8.2.tar.xz $ ./configure --prefix="$HOME/.local" $ make $ make test $ sudo make install $ echo "PATH=$HOME/.local/bin:\$PATH" > ~/.bashrc && source ~/.bashrc $ python3 -VライブラリのインストールCPythonはいくつかライブラリが入っているという話をしたので、一応入れておきましょう。$ pip3 install networkx numba numpy scipy何も考えることはないですね。 こちらとしてはインストールに難儀しました。 ./configure時に--prefixオプションを指定しないと、make install時に/usr/local/binに書き込もうとするので権限がなく、sudo make installするとpip installがrootでしか使えませんでした。 また、libssl-devを入れないとそもそもpip installできないし、libbz2-devがないとNetworkXが、libffi-devがないとがNumbaとSciPyが使えませんでした。 これでお好きなようにPythonのコードを書くことができます。VSCodeの拡張機能PythonPylancepython-snippetsVisual Studio IntelliCodeあたりを入れとけばいいんじゃないですかね。知らんけど。

> 内容を見る

NoImage Polygon
プログラミング

Privateの仕様を勘違いしてました

プログラミングできる人からしたら何言ってんだという内容かもしれませんが、結構ショックを受けたので残しておきます 先日、Kotlinのコードを読んでいたら、こんな感じのコードに出会いました class Foo(private var value: String) { operator fun plus(other: Foo): Foo { return Foo(this.value + other.value) } } パッと見普通のコードなのですが、このコードのために頭を抱えさせられました というのも、別のインスタンスのprivateプロパティにアクセスするコードが書かれているからです class Foo(private var value: String) { operator fun plus(other: Foo): Foo { return Foo(this.value + other.value) } } 例えば以下のようなコードが書けてしまいます fun main() { val foo1 = Foo("Hello") val foo2 = Foo("World") println(foo1 + foo2) } 結果 Foo: { value : HelloWorld } このコードでは、foo1とfoo2は、もともとのクラスは同じですが、全く別のインスタンスです。 しかし、このように書くことで、別のインスタンスのprivateなプロパティにもアクセスできるようになっています そこで調べてみたところ、以下の記述がありました(参照先) private はそのクラス内(そのすべてのメンバーを含む)でのみ見える よく見るとインスタンスではなく、クラス内のみアクセス可能と書いていました つまり同一のクラスからできたインスタンスだとアクセス可能らしいです だから最初のコードでもコンパイル&実行できるということらしいです しかし、ここでいくつかの疑問が出てきます。そこでそこらへんも調べてみました Privateなメソッド(関数)でも同じなのか 同じように動くようです(参考先) サブクラスだとどうなるのか 動きません 仮に、サブクラスも参照可能にするprotectedを利用しても不可能でした open class Foo(protected var value: String) class Bar(private val name: String): Foo(name){ // 引数のクラスの参照はNG fun ng(other: Foo) { println(other.value) } // 自身の親の参照はOK fun ok() { println(value) } } 拡張関数だとどうなるか 無理です open class Foo(private var value: String) // NG fun Foo.print(){ println(this.value) } これはJavaにコンパイルされたときのコードを見ればわかります public static final void print(@NotNull Foo $this$print) {   // Something... } Javaには拡張関数は存在しないため、引数としてクラスを渡して、それで処理しています ですので、privateプロパティにはアクセスできないので、動きません 自身のインスタンスだけアクセスできるようにする方法はないのか 少なくとも公式からは出されていない感じがします(privateが一番厳しい制約とのこと) Kotlinの場合、DelegateとかReflectionを使えば書ける気もしないこともないですが、書いたところでコンパイルエラーになりません したがってほとんど意味を成しません ですので、コメントやアノテーションなどで、分かるようにするのが精いっぱいだと思います 他の言語だとどうか C# using System; public class Program { public static void Main() { Foo foo1 = new Foo("Hello"); Foo foo2 = new Foo("World"); Console.WriteLine(foo1.getValue(foo2)); } } public class Foo { String value; public Foo(String value) { this.value = value; } public String getValue(Foo other) { return other.value; } } 結果 World 動いています PHP <?php $foo1 = new Foo("Hello"); $foo2 = new Foo("World"); print $foo1->getValue($foo2); class Foo {     private string $value;          function __construct(string $value)     {         $this->value = $value;     }          function getValue(Foo $other): string     {         return $other->value;     } } 結果 World 動いています ですので、Java、C#、PHPで動くため、おそらくほぼすべてのプログラミング言語はこの仕様だと考えられます まとめ 最初見たときは本当に驚きましたが、冷静になって考えるとこちらの方が理にかなっていますね 同一インスタンスじゃないとアクセスできない!としないといけない場面てほとんどないでしょうし むしろこちらの方が、クラスの役割をきっちりかけていい感じがします 今までずっと勘違いしてきたことを今更知って本当に恥ずかしいです・・・

> 内容を見る

プログラミング Polygon
プログラミング

岡大の新工学部HPのヘッダーを無理やり修正する

岡山大学工学部の新ホームページには、表示上の欠陥があります。 情報・電気・数理データサイエンス系とかいう欲張りで長すぎる名前が、「令和3年度〜新生工学部Webサイト」と被ってしまっています。 この現象は、画面の横幅が412px を下回った時に起こります。画像で記述されたロゴがどちらも透過画像であるため、文字が読みづらくなります。 本来は、HPを作成した岡山大学自然系研究科等事務部総務課に連絡すべきですし、今からしようと思いますが、成績が悪くて連絡しづらい夏季休業期間中ということで無理やり修正します。 修正する上での気を付けたことJavaScriptは使わない見た目の問題を後付けスクリプトでいじることはできるだけ避けたい行為です。 疑似要素を活用する既存の要素のプロパティ変更には限界があります。 例えば今回の場合、ロゴがpng画像なので、文字列を改行させるわけにはいきません。 そこで、before/after擬似要素を追加して、「令和3年度〜新生工学部Webサイト」を無理やり書き込みます。 Stylusを入れようStylusのダウンロード Google Chrome拡張の「Stylus」を入れます。Firefoxにも対応していますが、リンクは割愛します。 CSS:root {     --sublogo_width: 35vw;     --header_height: 37px; } #header .inr #logo {     position: relative; } #header .inr #logo a {     z-index: 4; } /* 本来のサブロゴは消す */ #header .inr .sp_subLOGO {     display: none; } h1#logo::after {     position: absolute;     top: 0;     display: none;     height: var(--header_height);     content: '令和3年度~新生工学部Webサイト';     align-items: center;     justify-content: flex-end;     right: 60px;     width: var(--sublogo_width);     font-size: 1rem;     text-align: right;     font-family: '游明朝','Yu Mincho',YuMincho,'Hiragino Mincho Pro',serif;     z-index: 3; } @media screen and (min-width: 310px) and (max-width: 1000px) {     h1#logo::after {         display: flex;     } } sublogo_width の35vw は完全に適当に決めているので、ベターな値があるかもしれません。 結果 疑似要素に「令和3年度〜新生工学部Webサイト」を書いているため、その横幅に応じて改行がされます。 スマホの横幅だといい感じに改行されて、メインロゴと被りません。なお、あまりに狭い場合はメディアクエリでサブロゴを隠します。 align-items: center; のおかげでいい感じに中央寄せできています。数年前にはできなかった選択肢。ありがとうflexbox!

> 内容を見る

プログラミング Polygon
プログラミング

Jetpack Composeで、Mutable系統をStateに登録したらあかん

タイトルで書いてあることを悟るまでに数日潰したので、怒鳴り散らしながらメモとして残します。 Jetpack ComposeとはAndroid開発において新たな時代の到来を感じさせるUI Toolkitです。 Androidは十数年前に登場して以降、様々なアプリが開発されてきています。 その影響もあり、Android開発を行うときは十数年前のクソみたいな古い風習が残っています。 例えばUIを作成するときです。 Androidでは命令型UIを採用しており、XMLと呼ばれる言語で書いていく必要がありました。 以下のような奴です<?xml version="1.0" encoding="utf-8"?> <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/LinearLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:background="@android:color/white" android:elevation="4dp" android:orientation="horizontal"> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/title" android:layout_weight="1" android:gravity="center" android:layout_gravity="center_vertical" android:textSize="30sp" android:layout_height="wrap_content" android:layout_width="0dp" android:clickable="true" android:text="@string/title" android:focusable="true" /> <androidx.appcompat.widget.AppCompatImageButton android:id="@+id/delete_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_delete_title" android:contentDescription="@string/delete" /> </androidx.appcompat.widget.LinearLayoutCompat> こんなコードから以下のようなUIが出力されます こんなのコードだけ見ても何を表しているのか、慣れていない人にとってはさっぱりわかりません。慣れててもよくわかりません。 これだけならいいのですが、ここからデータを表示するとなると、KotlinやJavaでコードを書く必要があります。 その書き方がかなりめんどくさいわけです。例えば、以下のような感じになります。 val binding = ChecklistTitleBinding.inflate(inflater, parent, false) val title: TextView = binding.title //要素の文字の部分の設定 title.apply { text = "Hello World" textSize = getTextSize(context).toFloat() //タイトル名をクリックした際の処理 setOnClickListener { clickTitleOnListener(title.text.toString()) } } この通り、部品一つずつ指定して書いていく必要があります。 これで、部品の名前とかはXMLで書いているのでそっちを見ないといけないし、指定するのを忘れていたらバグにつながります。 そこでJetpack Composeの出番です。 これは最近はやっている宣言的UIを採用しており、パッと見たらわかる感じになっています。 例えば、以下のように書くと、先ほど紹介したコードと同じことができます。 @Composable fun LazyCard(text: String = "Hello World"){ Card( modifier = Modifier .padding(10.dp) .fillMaxWidth() ) { Row( modifier = Modifier.padding(12.dp) ) { Text( text = text, modifier = Modifier.weight(1f) ) Icon( painter = painterResource(R.drawable.ic_baseline_delete_24), contentDescription = null ) } } } 以下のような感じですね これだと、コードを見ただけで、先ほどよりは断然分かりやすいと思います。 どのような見た目をしているかだったり、どこにデータが流れていくかであったりが分かりやすいので非常に使いやすいです。 このように、Jetpack Composeは、Androidの新時代を感じさせるUI Toolkitになっています。 本題やっと本題です。 Kotlinには複数のデータを扱うために、標準ライブラリの中にいくつかの型(class)が存在します 例えば配列を扱うためのList型、複数のPair型(keyとvalueを持った型)を扱うためのMap型などがそれにあたります。 これらの型は、一度値を作成したら値を追加、削除することができません しかし、これらを継承した型である、MutableList型、MutableMap型等、Mutable系統であれば可能ですval list = listOf("は", "ひ", "ふ") //コンパイルエラー list.add("へ") val mutableList = mutableListOf("は", "ひ", "ふ") //これはOK mutableList.add("へ") 結構扱いやすく、私もよく利用している型です。 そのため、Jetpack Composeにおいても何気なく使ったことが運の尽きでした。 ある日、こんな感じの、+ボタンを押したらカードが追加されていくアプリを作成しました。 ソースコードは以下の通りです。@Composable fun List(){ //副作用 var list by remember { mutableStateOf((0..3).toMutableList()) } //UI部分 LazyColumn(Modifier.fillMaxSize()){ item{ //カード表示部分 for(i in list){ LazyCard(text = i.toString()) } } item { //ボタン AddButton( //ボタンを押したときの処理 onClick = { val tempList = list tempList.add(4) list = tempList } ) } } } (副作用ってなんやねんて話ですが、話し出すと記事が一つできるような内容ですので省きますが、簡単に言うと、「関数外のところに影響を及ぼす部分」のことです。) このremember{}の部分が何をしているかというと、画面に表示するためのMutableListを保存しています。 rememberのところでState型を保有しておき、それを返してくれます。 これを変数に入れておき、値が変更されると、UIを更新してくれる優れものです。 今回の場合ですと、Listの中身が追加されれば、表示されるリストの数も追加されて以下のようになるはずです。 そして、画面内の+ボタンを押すと、Listの中身が追加されるようになっています。//ボタン AddButton( //ボタンを押したときの処理 onClick = { //別の変数に代入 val tempList = list //変数を追加   tempList.add(4) //listに反映 list = tempList } )つまり、このボタンを押せば増えてくれるはずです。 しかし、実際には増えてくれません。 この挙動のせいで数日間頭を抱える事態に陥ってしまいました 原因結論から書くと、rememberで渡された値が参照渡しになっていることでした。 私の意図した動きは以下の通りです//副作用の部分 var list by remember { mutableStateOf((0..3).toMutableList()) } AddButton( onClick = { //別の変数に、実際の値のコピーを代入 val tempList = list //tempListのリストに、変数を追加   tempList.add(4) //本体のlistに反映(ここで差分を検知してUIの再描画が走る) list = tempList } ) しかし、実際にはこう動いていました。//副作用の部分 var list by remember { mutableStateOf((0..3).toMutableList()) } AddButton( onClick = { //別の変数に、listの参照先を代入 val tempList = list //tempListのリストに、変数を追加(ここで、listの方にも変数が追加される。この追加方法ではUIの再描画が走らない)   tempList.add(4) //本体のlistとtempListの両方に値が追加されたせいで、差分が一切ないため、UIの再描画が走らない list = tempList } ) いろいろと調べてると、どうやらKotlin(というよりJava)では、クラス型は参照渡しを採用しているらしいです。 http://syrinx.q.t.u-tokyo.ac.jp/tori/java/basic/entity_ref.html Mutable系統もクラス型なので参照渡しということですね。 解決策値を新しく作って、ぶち込めば解決です。//副作用の部分 var list by remember { mutableStateOf((0..3).toList()) } AddButton( onClick = { list = list + listOf(list.size) } ) listに、新しくList型を作り直して、それをぶち込んでいます。 ここで重要なのが、Mutable型の特徴であるadd()関数では差分を検知してくれないということです。 ですので、Mutable型を使うことに対するメリットがないばかりか、意図せず使ってしまい、バグを誘発する危険性があります。 だからこそ、Mutable型をStateの部分(rememberのところ)では避けて、普通の型を使うべきでしょう。 以上 しかし、こんな記事俺以外に誰が見るんだろうか・・・

> 内容を見る

プログラミング Polygon
プログラミング

Twitterのタイムライン上の画像だけを表示したい!

こんにちは.小石こはです. 概要twitterのタイムライン上の画像をギャラリーのように表示させるまでの流れを紹介します. 使用技術Python3.9.5tweepyStreamlitTwitter API今回作成したもの以下の画像のように,タイムライン上の最新の画像を何件か取得して,ローカルWebサイト上に表示させるものを作成しました. ↓Twitterのタイムラインの様子 ↓今回作成した,ローカルwebサイト上にタイムライン上の画像を表示させたもの Twitter APIの利用申請,tweepyの導入Twitterからデータを取得したいので,Twitter APIの利用申請をします. Twitter API→ https://developer.twitter.com/en/docs/twitter-api 次に,Twitter APIを便利に使用するためのPythonのライブラリであるtweepyを以下のコマンドで導入します.pip install tweepy今回はローカルWebサイト上でTwitterのタイムライン上の画像をギャラリーのように表示させることを最終目標とします.他人に公開することはなく自分のみが閲覧するため,フロント実装が面倒になり,Streamlit(フロントエンドアプリケーションを作成できるPythonのフレームワーク)を使用することにしました. Streamlit→ https://streamlit.io/ 作成したプログラム作成したプログラムのディレクトリ構造は以下の通りです. main.pyの中身は以下の通りです. import streamlit as st from PIL import Image import streamlit.components.v1 as components from requests_oauthlib import OAuth1Session import tweepy import requests import json with open('twitter.json') as f: twitter_keys = json.load(f) def main():      # twitter.jsonからKEYを取得     CONSUMER_KEY = twitter_keys['consumer_key'] CONSUMER_SECRET = twitter_keys['consumer_secret'] ACCESS_TOKEN_KEY = twitter_keys['access_token'] ACCESS_TOKEN_SECLET = twitter_keys['access_token_secret'] auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) auth.set_access_token(ACCESS_TOKEN_KEY, ACCESS_TOKEN_SECLET) api = tweepy.API(auth, wait_on_rate_limit=True, wait_on_rate_limit_notify=True) public_tweets = api.home_timeline() # タイムラインの情報を取得 img_urls = []  # タイムラインの情報から,画像のURLを取得 for tweet in public_tweets: if 'media' in tweet.entities: for media in tweet.extended_entities['media']: media_url = media['media_url'] img_urls.append(media_url) # 取得した画像のURLをjpgに変換,imagesフォルダに保存 for i in range(0,len(img_urls)): url = img_urls[i] file_name = f"./images/{i}.jpg" response = requests.get(url) img = response.content with open(file_name, "wb") as f: f.write(img) # ローカルwebサイト上に画像を表示させる for i in range(0,len(img_urls)): img = Image.open(f'./images/{i}.jpg') st.image(img, use_column_width=True) if __name__ == "__main__": main() プログラムを実行するには以下のコマンドをたたきます. するとローカルサーバーが立ち上がります. streamlit run main.py これで完成です. Twitterを開かなくても,streamlit run main.pyとたたけばタイムライン上の画像を閲覧可能になりました. 以上です.ありがとうございました.

> 内容を見る