ソニックの部屋

主にプログラミングに関する記事を投稿します

「世界一流エンジニアの思考法」を読む

7つの要点のまとめ

  • 基礎の理解の徹底
  • 一番重要な一つだけをピックアップする
  • 自分の作業をクリアに説明できるようにする
  • 情報量を減らし重要な部分を説明する
  • 主体的に動き(タスクを振り)楽しむ
  • どんなにキリが悪くても絶対に定時で作業をやめる
  • 専門性を高め批判をしない

参考文献
牛尾 剛 (著)/文藝春秋/2023/世界一流エンジニアの思考法

JavaScriptまとめ

環境構築

  • warp(ターミナル)のインストール
  • 所定のフォルダで「code .」でvscode起動
  • vscode上でhtmlを作成し「!エンター」で様式入力
  • vscode上で拡張機能のlive serverをインストール
  • vscode上でhtml上で右クリックlive serverで起動
  • scriptタグに記述する
<script src="index.js"></script>
  • scriptタグはbodyの一番下に書く
  • 実務では別jsファイルに分ける
  • 実行結果はブラウザから確認する
  • 実行結果はターミナルから確認する
node index.js

文法

基礎

  • 以下(暗黙的な型変換により)false
undefined;
null;
0;
'';
NaN:
  • 関数は原則モダンなアロー関数を用いて、特に変更するものでもないのでconstを用いる
const hoge = () {
  // 処理
}
  • ウィンドウ全体のオブジェクトはwindow
// ポップアップを出す
window.alert('これはエラーです')
  • ページ全体のオブジェクトはdocument
// buttonタグを取得
document.getElementsByTagName('button')
  • イベントはevent
// buttonを押された時に処理
// addEventListenerは引数を2つ取る
document.getElementsByTagName('button').addEventListener('click', () => {
  // 処理
})
  • HTMLのid属性は一意となるためgetElementByIdを使って確実にデータを取得
// idを取得
// Elementとなりsは付かない
document.getElementById('xx')

// textContentに値を入れると書き換え可能
document.getElementById('xx').textContent = 'りんご'
  • HTMLのオブジェクトをとってくる場合は変数名に$を付けるのが慣習
const $button = document.getElementsByTagName('button')
  • eはイベントを表しe.targetでクリックしたボタンを取得する
const $button = document.getElementsByTagName('button')
$button[0].addEventListener('click', (e) => {
  // $button[0]を表す
  e.target
})
  • 文字列と変数は+で結合可能
  • グルーバル変数を汚染しないために即時関数で書く
(() => {
  // 処理
})()
  • HTML, CSS, JavaScriptを書く際の慣習
    • CSSはclass, JavaScriptはidであてる
    • idには"js-xx"のようにjs-を付ける
    • classやidを使いたくない時はdata-xx(データ属性)を使う
// querySelectorAllで全てのデータを取得
const $content = document.querySelectorAll('[data-content]')

// initで初期化
// style.displayでCSS操作
const init = () => {
  $content[0].style.display = 'block'
}
init()
  • e.preventDefault()でイベントを殺す(リンクに飛ばないなど)
  • e.target.dataset.xxxでデータ属性(data-xxx)の値を取得
  • classList.add('xxx')でクラス属性を全て取得し指定のクラス属性xxxを追加
  • classList.remove('xxx')でクラス属性を全て取得し指定のクラス属性xxxを削除
  • 同じ変数はなるべく一つにまとめる(保守性、パフォーマンスが増す)
  • DOMとはHTML文書内の要素のこと
  • xxx.nextElementSibilingでxxxの次の要素を取得
  • クラスの中の関数はconstなどはいらない
  • クラス内のthisはクラスを参照する
  • クラス内からクラス内のメソッドを呼び出すにはthis.メソッド名とする
  • strict modeで古い文法を排除し厳格かつ安全に実行できる
// 'use strict'という文字列をファイルまたは関数の先頭に書く
'user strict'
// 処理

変数・データ型

  • constは厳密には定数ではなく再代入できない変数
  • 複数行の文字列は``で囲む
`
aaa
bbb
ccc
`
  • 数値から文字列の変換はString()
String(1); // => "1"
  • 文字列から数値の変換はNumber()
// ユーザー入力を文字列として受け取る
const input = window.prompt("数字を入力してください", "42");
// 文字列を数値に変換する
const num = Number(input);
  • プリミティブ値は参照先の値、オブジェクトは参照がコピーされる

関数

  • Rest parametersは、仮引数名の前に...をつけた仮引数のことで、配列となる
function fn(...args) {
    // argsは、渡された引数が入った配列
    console.log(args); // => ["a", "b", "c"]
}
fn("a", "b", "c");
  • Arrow Functionの省略記法
// 関数の仮引数が1つのときは()を省略できる
const fnC = x => {};
// 関数の処理が1つの式である場合に、ブロックとreturn文を省略できる
const mulB = x => x * x;
  • 同じ名前の関数を複数回宣言した場合には、後ろで宣言された関数によって上書きされるため注意
  • 引数として渡される関数のことをコールバック関数
  • コールバック関数を引数として使う関数やメソッドのことを高階関数
function 高階関数(コールバック関数) {
    コールバック関数();
}
// コールバック関数となる無名関数をその場で定義して渡せます。
const array = [1, 2, 3];
array.forEach((value) => {
    console.log(value);
});
  • メソッドを呼び出す場合は、関数呼び出しと同様にオブジェクト.メソッド名()
const obj = {
    method: function() {
        return "this is method";
    }
};

// 短縮記法
const obj = {
    method() {
        return "this is method";
    }
};

console.log(obj.method()); // => "this is method"
  • ブロックで終わる文の末尾には、セミコロンが不要
  • 渡す引数と受け取る引数は引数の順番で決まる(キーワード引数はない)
  • 関数は実行可能なオブジェクト
  • シンボルはプロパティーの重複を避けるために必ず一意の値を返す関数
const s = Symbol();

ループ

  • 配列のforEachメソッド
// 配列の繰り返し処理
const array = [1, 2, 3]
array.forEach(value => {
  console.log(value);
});
  • 配列のsomeメソッド
// 配列の各要素をテストする
function isEven(num) {
    return num % 2 === 0;
}
const numbers = [1, 5, 10, 15, 20];
console.log(numbers.some(isEven)); // => true
  • 配列のfilterメソッド
// 配列から特定の値だけを集めた新しい配列を作る
function isEven(num) {
  return num % 2 === 0;
}
const array = [1, 5, 10, 15, 20]
console.log(array.filter(isEven)); // => [10, 20]
  • for...in文の利用は避け、Object.keysメソッドなどを使って配列として反復処理するなど別の方法を考えたほうがよい
// for...in文は親オブジェクトのプロパティも引き継ぐためObject.keysメソッドを使う
const obj = {
  'a': 1,
  'b': 2,
  'c': 3
};
Object.keys(obj).forEach(key => {
  const value = obj[key];
  console.log(`key:${key}, value:${value}`);
});

// 非推奨
for (const i in obj) {
  console.log(i); // a,b,c
  console.log(obj[i]); // 1,2,3
}
  • 配列の繰り返しにはforEachの他にfor of分も使える
const array = [1, 2, 3];
for (const value of array) {
  console.log(value);
}
  • for文などの構文ではcontinue文やbreak文が利用できるが、配列のメソッドではそれらは利用できない
  • 一方で配列のメソッドは、一時的な変数を管理する必要がないことや、処理をコールバック関数として書くという違いがある
/ object map array set
キー 文字列 指定なし 重複OK 重複NG
for...in o x o x
for...of x o o o
  • mapの使い方
const map = new Map();
const key1 = {};

map.set(key1, 'value1');
console.log(map.get(key1)) // value1
// arry1とarry2は違う配列となる
const arry1 = [1,2,3,4,5];
const arry2 = [...arry1];

// スレッド演算子は引数となる場合はrest parameterという
function sum(...args) {
  let r = 0;
  for(let v of args) {
    r += v;
  }
  return r;
}
const result = sum(1,2,3,4);
console.log(result); // 10

オブジェクト

  • オブジェクトはプロパティの集合であり、プロパティとは名前(キー)と値(バリュー)が対になったもの
  • プロパティへのアクセスは基本的には簡潔なドット記法(.)を使い、ドット記法で書けない場合はブラケット記法([])を使う
const obj = {
  key: value
}
console.log(obj.key)
console.log(obj['key'])

// 削除はdeleteを使う
delete obj.key
  • プロパティを初期化時以外に追加してしまうと、そのオブジェクトがどのようなプロパティを持っているかがわかりにくくなる。 そのため、できる限り作成後に新しいプロパティは追加しないほうがよい
  • プロパティの存在確認にはinを使う
const obj = {
  key: undefined
};
if ('key' in obj) {
//処理
}
  • オブジェクトの列挙方法
const obj = {
    "one": 1,
    "two": 2,
    "three": 3
};
// `Object.keys`はキーを列挙した配列を返す
console.log(Object.keys(obj)); // => ["one", "two", "three"]
// `Object.values`は値を列挙した配列を返す
console.log(Object.values(obj)); // => [1, 2, 3]
// `Object.entries`は[キー, 値]の配列を返す
console.log(Object.entries(obj)); // => [["one", 1], ["two", 2], ["three", 3]]
  • オブジェクトのマージ
    • 空のオブジェクトをtargetにすることで、既存のオブジェクトには影響を与えずマージしたオブジェクトを作ることができる。 そのため、Object.assignメソッドの第一引数には、空のオブジェクトリテラルを指定するのが典型的な利用方法
const objA = {a: 'a'};
const objB = {b: 'b'};
const merged = Object.assign({}, objA, objB);
console.log(merged); // => {a: 'a', b; 'b'}
  • メソッドのthisはオブジェクトを参照する
  • 関数のthisはグルーバルオブジェクトを参照する
  • bindによりthisの参照先を変更することができる(実行しない)
  • call, applyによりthisの参照先を変更することができる(実行する)
  • コンストラクターへの関数の追加はメモリ効率の観点からprototypeを使う→現在はクラスで代替する
// コンストラクターは大文字から始める
function Person(){
  this.name = name;
}

Preson.prototype.hello = function(){
 // 処理 
}

const bob = new Person('Bob', 18);
bob.hello();
if (arg instanceof Array) {
  // 処理
}
class Person {
  constructor(name, age) {
    this._name = name;
    this._age = age;
}

  get name() {
    // 何らかの処理
    return this._name;
}

  set name(val) {
    // 何らかの処理
    this._name = val;
}
}

const p = new Person('Bob', 18);
console.log(p.name) // get経由で呼ばれる
p.name = 'Alice' // set経由で呼ばれる

配列

  • 配列から条件に一致する要素を取得
const colors = [
  {'color': 'red'},
  {'color': 'blue'},
  {'color': 'green'},
  {'color': 'blue'}
]
// 条件に一致する最初の要素を取得
const blueColor = colors.find((obj) => {
  return obj.color === 'blue';
});
console.log(blueColor); // => {'color': 'blue'}
  • 配列から真偽値を取得(配列に含まれているかどうかチェック)
const array = ["Java", "JavaScript", "Ruby"];
// `includes`は含まれているなら`true`を返す
if (array.includes("JavaScript")) {
    console.log("配列にJavaScriptが含まれている");
}
  • 配列の結合
const array = ['A', 'B', 'C'];
const newArray = array.concat(['D', 'E']);
console.log(newArray); // => ['A', 'B', 'C', 'D', 'E']
  • 配列は破壊的な変更かどうか意識する必要がある
破壊的な方法 非破壊な方法
array[index] = item Array.prototype.with
Array.prototype.pop array.slice(0, -1)とarray.at(-1)
Array.prototype.push [...array, item]
Array.prototype.splice Array.prototype.toSpliced
Array.prototype.reverse Array.prototype.toReversed
Array.prototype.sort Array.prototype.toSorted
Array.prototype.shift array.slice(1)とarray.at(0)
Array.prototype.unshift [item, ...array]
Array.prototype.copyWithin なし
Array.prototype.fill なし
  • 破壊的なメソッドは、シンプルだが元の配列も変更してしまうため、意図しない副作用が発生しバグの原因となる可能性あり。 非破壊的なメソッドは、使い分けが必要だが元の配列を変更せずに新しい配列を返すため、副作用が発生することはない。そのため、まず非破壊的な方法で書けるかを検討し、そうではない場合に破壊的な方法を利用するとよい
  • 改めて配列の繰り返し処理
// コールバック関数には要素, インデックス, 配列が引数として渡される
const array = [1, 2, 3];
array.forEach((currentValue, index, array) => {
    console.log(currentValue, index, array);
});
// コンソールの出力
// 1, 0, [1, 2, 3]
// 2, 1, [1, 2, 3]
// 3, 2, [1, 2, 3]
// mapメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数が返した値から新しい配列を返す非破壊的なメソッド
const array = [1, 2, 3];
// 各要素に10を乗算した新しい配列を作成する
const newArray = array.map((currentValue, index, array) => {
    return currentValue * 10;
});
console.log(newArray); // => [10, 20, 30]
// 元の配列とは異なるインスタンス
console.log(array === newArray); // => false
// filterメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数がtrueを返した要素だけを集めた新しい配列を返す非破壊的なメソッド
const array = [1, 2, 3];
// 奇数の値を持つ要素だけを集めた配列を返す
const newArray = array.filter((currentValue, index, array) => {
    return currentValue % 2 === 1;
});
console.log(newArray); // => [1, 3]
// 元の配列とは異なるインスタンス
console.log(array === newArray); // => false

文字列

  • 正規表現は柔軟で便利だが、コード上から意図が消えてしまいやすい。 そのため、正規表現を扱う際にはコメントや変数名で具体的な意図を補足したほうがよい
  • Stringメソッドで表現できることはStringメソッドで表現し、柔軟性や曖昧な検索が必要な場合はコメントとともに正規表現を利用する
  • 文字列の置換
// 検索対象となる文字列
const str = "???";
// replaceメソッドに文字列を指定した場合は、最初に一致したものだけが置換される
console.log(str.replace("?", "!")); // => "!??"
// replaceAllメソッドに文字列を指定した場合は、一致したものがすべて置換される
console.log(str.replaceAll("?", "!")); // => "!!!"

スコープ

  • 内側から外側のスコープへと順番に変数が定義されているか探す仕組みのことをスコープチェーン
  • クロージャ
    • javascriptは静的スコープ: ある変数がどの値を参照するかは静的に決まる
    • javascriptのメモリ管理の仕組み: 参照されなくなったデータはガベージコレクションにより解放される(反対に参照し続けることで状態を保持する)
    • クロージャーは上記2つの性質を利用して、関数内から特定の変数を参照し続けることで関数が状態を持てる仕組みのこと
    • クロージャーによりプライベート変数を定義できる
  • レキシカルスコープとはコードを書く場所によって参照できる変数が変わるスコープのこと
  • ホイスティングとは変数や関数の定義をコード実行前にメモリに配置すること
  • 即時関数とは宣言と同時に一度だけ実行される関数のこと
    • 即時関数内でしか使えない変数や関数と、即時関数外でも使える変数や関数を定義できる

this

  • thisが実際に使われるのはメソッド
  • 関数の種類
// `function`キーワードからはじめる関数宣言
function fn1() {}
// `function`を式として扱う関数式
const fn2 = function() {};
// Arrow Functionを使った関数式
const fn3 = () => {};
  • JavaScriptではオブジェクトのプロパティが関数である場合にそれをメソッドと呼ぶ
const obj = {
    // `function`キーワードを使ったメソッド
    method1: function() {
    },
    // Arrow Functionを使ったメソッド
    method2: () => {
    }
    // メソッドの短縮記法で定義したメソッド
    method() {
    }
};

// メソッド呼び出し
obj.method();
  • Arrow Function以外におけるthisは「ベースオブジェクト。 ベースオブジェクトとはメソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクトのこと。 ベースオブジェクトがない場合のthisはundefined」
// `fn`関数はメソッドではないのでベースオブジェクトはない
fn();
// `obj.method`メソッドのベースオブジェクトは`obj`
obj.method();
  • Arrow Functionにおけるthisは「自身の外側のスコープにあるもっとも近い関数のthisの値」
function outer() {
    // Arrow Functionで定義した関数を返す
    return () => {
        // この関数の外側には`outer`関数が存在する
        // `outer`関数に`this`を書いた場合と同じ
        return this;
    };
}
// `outer`関数の返り値はArrow Functionにて定義された関数
const innerArrowFunction = outer();
console.log(innerArrowFunction()); // => undefined
  • thisは状況によって異なる値を参照するため注意
  • コンストラクタ関数内でのthisはこれから新しく作るインスタンスオブジェクト
class Point {
  constructor(x, y) {
  this.x = x;
  this.y = y;
  }
}
  • チェーンメソッドをするにはメソッドでthisを返す
class Person {
  hello() {
    // 処理
    return this;
}
  introduce() {
    //処理
}
const bob = new Person()
bob.hello().introduce()  // 繋げて書ける
}

クラス

  • クラス名は大文字ではじめる
  • クラスのメソッド(プロトタイプメソッド)
class クラス {
    メソッド() {
        // このメソッドはプロトタイプメソッドとして定義される
    }
}

// クラスでは次のようにメソッドを定義できない
class クラス {
   // SyntaxError
   メソッド: () => {}
   // SyntaxError
   メソッド: function(){}
}

const a = new クラス()
  • 継承
class 子クラス extends 親クラス {
}
  • コンストラクタの処理順は親クラスから子クラスへ
class Japanese extends Person {
  constructor(name, age, gender) {
    super(name, age);  // 親の変数を呼ぶ
    this.gender = gender;
}
  hello() {
    super.hello();  // 親のメソッドを呼ぶ
    // 処理
}
}
  • クラスはコンストラクタ関数をクラス表記で書けるようにしたもの

例外処理

  • 例外はtry...catch
try {
    console.log("try節:この行は実行されます");
    // 未定義の関数を呼び出してReferenceError例外が発生する
    undefinedFunction();
    // 例外が発生したため、この行は実行されません
} catch (error) {
    // 例外が発生したあとはこのブロックが実行される
    console.log("catch節:この行は実行されます");
    console.log(error instanceof ReferenceError); // => true
    console.log(error.message); // => "undefinedFunction is not defined"
} finally {
    // このブロックは例外の発生に関係なく必ず実行される
    console.log("finally節:この行は実行されます");
}
  • throw文により例外を投げる
try {
    // 例外を投げる
    throw new Error("例外が投げられました");
} catch (error) {
    // catch節のスコープでerrorにアクセスできる
    console.log(error.message); // => "例外が投げられました"
}

非同期処理

  • 同期処理ではコードを順番に処理していき、ひとつの処理が終わるまで次の処理は行わない。 同期処理では実行している処理はひとつだけとなるため、とても直感的な動作となる
  • 非同期処理はコードを順番に処理していくが、ひとつの非同期処理が終わるのを待たずに次の処理を評価する。 つまり、非同期処理では同時に実行している処理が複数ある
  • JavaScriptでは一部の例外を除き非同期処理が並行処理(Concurrent)として扱われる。 並行処理とは、処理を一定の単位ごとに分けて処理を切り替えながら実行すること
  • 並列処理とは、排他的に複数の処理を同時に実行すること
  • Promiseにより非同期処理の状態や結果を表現する
// asyncPromiseTask関数は、Promiseインスタンスを返す
function asyncPromiseTask() {
    return new Promise((resolve, reject) => {
        // さまざまな非同期処理を行う
        // 非同期処理に成功した場合は、resolveを呼ぶ
        // 非同期処理に失敗した場合は、rejectを呼ぶ
    });
}
// asyncPromiseTask関数の非同期処理が成功した時、失敗した時に呼ばれる処理をコールバック関数として登録する
asyncPromiseTask().then(()=> {
    // 非同期処理が成功したときの処理
}).catch(() => {
    // 非同期処理が失敗したときの処理
});
async function doAsync() {
    return "値";
}
// doAsync関数はPromiseを返す
doAsync().then(value => {
    console.log(value); // => "値"
});
  • await式は右辺のPromiseインスタンスがFulfilledまたはRejectedになるまでその場で非同期処理の完了を待つ。 そしてPromiseインスタンスの状態が変わると、次の行の処理を再開する。
  • await式を使うことで非同期処理が同期処理のように上から下へと順番に実行するような処理順で書ける。
async function asyncMain() {
    // PromiseがFulfilledまたはRejectedとなるまで待つ
    await Promiseインスタンス;
    // Promiseインスタンスの状態が変わったら処理を再開する
}
  • キューは実行待ちの行列となり、キューの中でタスクを管理する(非同期処理の実行順を管理)
  • Promiseは簡単に可読性が上がるように非同期処理を書けるにようにしたもの
  • Promiseは非同期のチェーン処理に適した書き方
new Promise(function(resolve, reject) {
  // 同期処理
  resolve('hello');
}).then(function(data) {
  // 非同期処理
  // resolveの時に処理される
}).catch(function(data) {
  // 非同期処理
  // rejectの時に処理される(エラー処理を書く)
}).finally(function() {
  // 非同期処理
  // 終了処理
});
  • async/awaitはPromiseを直感的に記述できるようにしたもの
  • asyncはPromiseを返却する関数宣言を行う
  • awaitはasyncの非同期処理が完了するまで待つ
  • 関数の中でawaitがある場合は必ず非同期処理となりその関数は必ずasyncを付ける
async function init() {
  let val = await sleep(0);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
}

その他

  • Mapはキーと値の組み合わせからなるコレクションを扱うビルトインオブジェクト
// 定義
const map = new Map();
const map = new Map([["key1", "value1"], ["key2", "value2"]]);
// 新しい要素の追加
map.set("key", "value1");
// キーの存在確認
console.log(map.has("key")); // => true
// 反復処理
const results = [];
map.forEach((value, key) => {
    results.push(`${key}:${value}`);
});
console.log(results); // => ["key1:value1","key2:value2"]
  • Setは重複する値がないことを保証した順序を持たないコレクションを扱うビルトインオブジェクト
  • JSONは文字列
  • JSONをオブジェクトに変換する
// JSONはダブルクォートのみを許容する
const json = '{ "id": 1, "name": "js-primer" }';
const obj = JSON.parse(json);
console.log(obj.id); // => 1
console.log(obj.name); // => "js-primer"
  • オブジェクトをJSONに変換する
const obj = { id: 1, name: "js-primer", bio: null };
console.log(JSON.stringify(obj)); // => '{"id":1,"name":"js-primer","bio":null}'
実行
  • 時刻値を取得する
// 現在の時刻を表すインスタンスを作成する
const now = new Date();
// 時刻値を取得する
console.log(now.getTime());
  • ただし、JavaScriptにおける日付・時刻の処理は、標準のDateではなくライブラリを使うことが一般的
  • 乱数を生成する
for (let i=0; i < 5; i++) {
    // 毎回ランダムな浮動小数点数を返す
    console.log(Math.random());
}
// 名前つきエクスポートされたfooとbarをインポートする
import { foo, bar } from "./my-module.js";
console.log(foo); // => "foo"
console.log(bar); // => function bar()

アプリ作成時の気付き

  • エントリーポイントとは、アプリケーションの中で一番最初に呼び出される部分のこと
  • Fetch APIはHTTP通信を行ってリソースを取得するためのAPI
  • fetch
    • Promiseを返す
    • thenメソッドが使える(Promiseを返すから)
    • jsonメソッドが使える
// HTTPリクエスト
const userId = "任意のGitHubユーザーID";
fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`);
// HTTPレスポンス
const userId = "js-primer-example";
fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
    .then(response => {
        console.log(response.status); // => 200
        return response.json().then(userInfo => {
            // JSONパースされたオブジェクトが渡される
            console.log(userInfo); // => {...}
        });
    });
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";

// コマンドライン引数をcommanderでパースする
program.parse(process.argv);

// ファイルパスをprogram.args配列から取り出す
const filePath = program.args[0];
  • Node.jsでファイルの読み書きを行うには、標準モジュールのfsモジュールを使う
  • Node.jsの標準モジュールはnode:fsのようにnode:プリフィックスをつけてインポートできる。 プリフィックスを付けないfsでもインポートできるが、npmからインストールしたサードパーティ製のモジュールとの区別が明確になるため、付けておくことが推奨
// fs/promisesモジュール全体を読み込む
import * as fs from "node:fs/promises";

// fs/promisesモジュールからreadFile関数を読み込む
import { readFile } from "node:fs/promises";
  • ファイルの読み書きは存在の有無や権限、ファイルシステムの違いなどによって例外が発生しやすいので、必ずエラーハンドリング処理を書く
  • markedパッケージを使ってMarkdown文字列をHTML文字列に変換可能
  • テスティングフレームワークのmochaパッケージをインストールし、npm testコマンドでmochaコマンドを実行することによりテストが可能
$ npm test
> mocha test/
  • event.preventDefaultメソッドは、submitイベントの発生元であるフォームが持つデフォルトの動作をキャンセルする
    • フォームが持つデフォルトの動作とは、フォームの内容を指定したURLへ送信するという動作
    • form要素に送信先が指定されていないため、現在のURLに対してフォームの内容を送信
    • 現在のURLに対してフォームの送信が行われると、結果的にページがリロードされる
    • リロードさせないためにevent.preventDefaultメソッドを使う
formElement.addEventListener("submit", (event) => {
    // submitイベントの本来の動作を止める
    event.preventDefault();
    console.log(`入力欄の値: ${inputElement.value}`);
});
  • イベントが発生したことを元に処理を進める方法をイベント駆動(イベントドリブン)
  • commanderでコマンドライン引数にオプションを付ける
// コマンドライン引数の取得とオプションの設定
import {program} from 'commander';
program.option('-m, --month <number>');
program.parse(process.argv);
const options = program.opts();
console.log(options.month;);
  • 異常終了(0は正常終了)
process.exit(1);
  • 改行せずに表示
process.stdout.write('xxx');
  • 数字の二桁表示
// iは数字
i = i.toString().padStart(2, '0');
  • 複数の空白の作り方
// 空白数を返す
let fn_blank = (num) => {
  return '   '.repeat(num);
}
  • Webブラウザでモジュールを使用するためには、scriptタグにtype="module"属性を追加
<body>
  <h1>ToDo List</h1>
  <script type="module" src="./index.js"></script>
</body>
  • プライベートフィールドは#を付ける
// TodoListModelはAppクラスの外からは触る必要がないためプライベートフィールドとする
export class App {
  #todoListModel = new TodoListModel();
}
  • confirmで削除前のメッセージを表示させる
// OKならtrue、キャンセルならfalseを返す
let result = confirm('削除OK?')

参考文献
happiness-chain教材
JavaScript入門・完全版コース/プログラミング初心者向け、コスパ最強講座
JavaScript Primer
Commander.jsを使用してNode.jsのCLI引数を処理する
Node.jsで終了ステータスコードを使うなら押さえておきたいasyncと終了ステータスコードの話
Node.jsでコンソールの表示を上書きする
【JavaScript】padStartメソッドで0詰め2桁の数字を作る関数
JSのプライベートフィールドは接頭辞に#(シャープ)を付けて表示する
JavaScriptによる削除する前に確認メッセージ
CodeMafia, 2024, 「【JS】ガチで学びたい人のためのJavaScriptカニズム」, udemy, (2024/4/16取得,https://www.udemy.com/).

ソフトウェアテスト技法まとめ

ソフトウェアテスト技法

  • 目標は なるべく少ないテストケースでなるべく多くのバグを発見する
  • テストの種類
  • テスト設計アプローチ
    • 以下どちらでテストするかを明確にする
    • 契約によるテスト
      • 事前条件の範囲内だけでテストする
    • 防御的テスト
      • 事前条件の範囲外もテストする

同値クラステスト

  • テスト的には同じ意味になる値のこと
  • 同値クラスの分だけテストケースを用意すれば良い

境界値テスト

ドメイン分析テスト

  • 複数の条件がある時に有効
  • 2つの条件(変数)が相互作用を持つ場合に適している
  • ドメイン内は条件を満たす範囲のこと
  • onポイントは境界値のこと
  • offポイントは境界値に隣接する値のこと
  • onポイントがドメイン内ならoffポイントはドメイン外とする
  • onポイントがドメイン外ならoffポイントはドメイン内とする
  • ドメインテストマトリクスで網羅性を確保できる
  • in(ドメイン内の満たす値)を固定しonとoffを調整

※1,2列は算数の点数>=60の条件を検証している
列がテストケース(下記例は4ケース)

変数 条件 ポイント 1 2 3 4
算数 算数の点数>=60 on 60 - - -
off - 59 - -
in - - 70 80
合計点 (算数の点数+国語の点数)>=90 on - - 70/20 -
off - - - 80/9
in 60/40 59/41 - -
期待される結果 合格 不合格 合格 不合格

デシジョンテーブル

  • 複数の条件がある時に有効
  • 2つの条件(変数)が独立している場合に適している
  • それは論理式で表すことができるもの
  • テストケースの圧縮(ケースを減らす)を考える

※上からNoが出たら以降の判定は行わないのがコツ
列がテストケース(下記例は4ケース)

1 2 3 4
条件
書籍を購入している Yes Yes Yes No
4000円以上購入している Yes Yes No -
離島に住んでいない Yes No - -
アクション
送料無料 Yes No No No

ペア構成テスト

  • もっと複雑、多くのテストケースがある時に有効
  • 全ての組み合わせではなく条件の全てのペアをテストする
  • 70~85%程度のバグを発見可能
  • 直交表(列が条件、行がテストケース)を使う

※例:L4(23)・・・2値条件が3つ
行がテストケース(下記例は4ケース)

A B C
1 0 0 0
2 0 1 1
3 1 0 1
4 1 1 0

状態遷移テスト

  • もっと複雑、多くのテストケースがある時に有効
  • 状態と遷移がある時に有効
  • 1.まずは状態遷移図で全体感を把握
  • 2.次に状態遷移表を使って全ての遷移を確認
  • 3.その後Nスイッチカバレッジにより段階的な遷移をテスト
  • Nスイッチカバレッジ
    • Nは前状態から後状態までに経由する状態(スイッチ)のこと
    • 例えば停止中→初期状態(スイッチ)→動作中
    • 状態遷移表を前状態(行)、後状態(列)の行列にする
    • Nが増えるとテストケースが増える

※以下は状態遷移表
※列→行

イベント\状態 ①初期状態 ②動作中 ③停止中
スタートボタン
リセットボタン N/A N/A

※以下は1スイッチカバレッジ
※イベントRはリセット、Sはスタート
※+はorの意味
※行→イベント→列へ遷移
行列がテストケース(下記例は12ケース)

前状態\後状態 ①初期状態 ②動作中 ③停止中
①初期状態 RR+SR RS SS
②動作中 RR+SR RS+SS -
③停止中 RR+SR RS SS

テスト技法以外のテストケースを設計する観点としてカバレッジ

  • ホワイトボックステスト
    • データフローテスト
      • 変数毎に生成→使用→廃棄の順番が守られているか確認
    • 制御フローテスト
      • 100の内何%が実行されたかに注目してテストを行う(=カバレッジ
  • カバレッジレベルの求め方の種類(低い順に)
    • ステートメントカバレッジ(C0)
      • 実行可能な行のうち何行を実行したか
      • Webアプリで採用
    • ディシジョンカバレッジ(C1)
      • 判定の分岐をどれだけ網羅したか
      • 100%ディシジョンカバレッジ=100%C0
      • Webアプリで採用
    • コンディションカバレッジ(C2)
      • 判定の中の条件式の真偽をどれだけ網羅したか
    • マルチプルコンディションカバレッジ(MCC)
      • 条件式の真偽の組み合わせをどれだけ網羅したか
    • MC/DCカバレッジ
      • MCCを改良しテストケースを削減したもの
      • 100%MC/DCカバレッジ=100%C0, C1, C2
      • 銀行システムなど重要システムに採用
  • カバレッジ計算は自動計測ツールを用いる
  • ポイントは 効果の高いテスト技法を採用した結果、高いカバレッジが副次的に得られる ということであり、カバレッジの上昇が目的ではない
  • 85%程度のカバレッジが努力目標

参考文献
ひらまつ しょうたろう, 2023, 「はじめてのソフトウェアテスト技法【全てのエンジニアが知るべき最重要テスト技法を、丁寧な解説と演習問題で身につけよう】」, udemy, (2024/2/20取得,https://www.udemy.com/).

Djangまとめ

Django基礎

Udemy(Django3.0)

フレームワークとは

  • ウェブサイトを効率的に作るためのもの

Hello worldアプリ

  • プロジェクトを作る
django-admin startproject helloworldproject . ←.を付けることでフォルダを一つ省略する
  • ローカルサーバの立ち上げ
python manage.py runserver
  • settings.py

    • DEBUG=Falseで本番環境となる
    • ALLOWED_HOSTS=[]で本番環境の際に拒否するホストの設定ができる
    • INSTALLED_APPSはDjango内のアプリ
    • MIDDLEWAREはDjangoのセキュリティの設定
    • TEMPLATESのDIRSはHTMLファイルの保存先
  • urls.py

    • ルーティング
    • 上から順番にパスを拾っていく性質
    • オブジェクト(データ)を受け取る
    • views.pyのクラス又は関数を呼び出す
    • classを呼び出す場合は.as_view()を付ける(オブジェクトを作成して関数に変えるイメージ)
urlpatterns = [
  path('helloworld2/', HelloWorldClass.as_view())
]
  • views.py

    • コントローラー
    • function based view(古いキッチン)
    • class based view(新しいキッチン)
      • template_name=xxでビューのhtmlファイルを指定
  • アプリ

    • urls.py, views.py, models.pyの固まり
    • Djangoはプロジェクト(settings.py, urls.py)とアプリに大きく分類できる
# ①アプリの作成
python manage.py startapp helloworldapp(=アプリ名)

# ②Djangoのプロジェクトに対してアプリを作成したことを伝える
# settings.py
INSTALLED_APPS = [
    ...
    'helloworldapp.apps.HelloworldappConfig', # 一般的な書き方
]

# ③アプリとプロジェクトの繋ぎ
# urls.py
from django.urls import path, include

urlpatterns = [
    path('', include('helloworldapp.urls')), # helloworldapp/だと重複となりエラーとなるので注意
]
  • プロジェクトは統括しアプリに指示を出すもの
  • プロジェクトは全体の設定、アプリは商品のアプリ、支払いのアプリ等のイメージ
  • urls.pyはデフォルトではアプリに入っていないがアプリにも作るのが一般的

Todoアプリ

  • models.py
# TodoModelテーブル
class TodoModel(models.Model):
    title = models.CharField(max_length=100) # フィールドの設定
    memo = models.TextField()
  • migrateの実行
# ①設計図の作成
python manage.py makemigrations (アプリ名) # (アプリ名)は省略可
# ②テーブルの作成
python manage.py migrate
  • 管理画面
    • superuserの作成
python manage.py createsuperuser
# admin.py
# ①管理画面にデータの追加
from .models import TodoModel

admin.site.register(TodoModel)

# models.py
# ②データ名の変更
def __str__(self):
  # オブジェクトを作成した際に文字列を返す
  return self.title
  • CRUD

    • Create: CreateView
    • Read: ListView, DetailView
    • Update: UpdateView
    • Delete: DeleteView
  • ListVeiw

    • 一覧表示
# views.py
from django.views.generic.list import ListView
from .models import TodoModel

class TodoList(ListView):
    template_name = 'list.html'
    model = TodoModel
# list.html
<!-- object_listは指定したmodel(TodoModel)の全てのデータ -->
{% for post in object_list %}
<ul>
  <li>{{ post.title }}</li>
  <li>{{ post.memo }}</li>
</ul>
{% endfor %}
  • DetailView
    • 個別表示
# urls.py
urlpatterns = [
    path('list/', TodoList.as_view()),
    path('detail/<int:pk>', TodoDetail.as_view()), # <int:pk>がポイント
]
# detail.html
{{ object.title }}
  • 共通のビューはbase.htmlに集約する
    • 箱(block header等)を作って各々のビューで箱の中に記述していく
# base.html
{% block header %}
{% endblock %}
# list.html
{% extends 'base.html' %} ←base.htmlを読み込み
{% block header %}
this is list.
{% endblock %}
  • 中身の優先度に応じて色を替える
    • ①classにフィールド名を記述する
# list.html
{% block content %}
<div class="container">
  {% for item in object_list %}
  <div class="alert alert-{{ item.priority }}" role="alert"> ←{{ item.priority }}がポイント
    <p>{{ item.title }}</p>
    <a href="#" class="btn btn-info" tabindex="-1" role="button" aria-disabled="true">編集画面へ</a>
    <a href="#" class="btn btn-success" tabindex="-1" role="button" aria-disabled="true">削除画面へ</a>
    <a href="#" class="btn btn-primary" tabindex="-1" role="button" aria-disabled="true">詳細画面へ</a>
  </div>
  {% endfor %}
</div>
{% endblock %}
  • ②モデルの中でユーザーが選択できるフィールドを追加する
# 右のデータは管理画面の表示名、左のデータはBootstrapより
CHOICE = (('danger', 'high'), ('warning', 'normal'), ('primary', 'low'))

# TodoModelテーブル
class TodoModel(models.Model):

    priority = models.CharField(
        max_length=50,
        choices=CHOICE, # 管理画面上で選択肢を与える
        )
    duedate = models.DateField() # 時間の表示(本件には関係ない) 
  • マイグレーションファイルの作成とマイグレートによるテーブルの作成

    • 何らかのデータを入れる警告が出る(nullはNG)→timezone.now, dangerを入力
  • CreateView

    • ①ルーティングの設定
# urls.py
from .views import TodoList, TodoDetail, TodoCreate

urlpatterns = [
    path('list/', TodoList.as_view(), name='list'),  # nameを記述することにより画面遷移が可能となる
    path('detail/<int:pk>', TodoDetail.as_view(), name='detail'),
    path('create/', TodoCreate.as_view(), name='create'),
]
  • ②コントローラーの設定
    • reverseは名前からビューを呼び出す
# views.py
from django.views.generic import ListView, DetailView, CreateView
from django.urls import reverse_lazy

class TodoCreate(CreateView):
    template_name = 'create.html'
    model = TodoModel
    fields = ('title', 'memo', 'priority', 'duedate') # フォームの設定
    success_url = reverse_lazy('list') # データ登録後listに画面遷移
  • ③ビューの設定
# create.html
{% extends 'base.html' %}

{% block content %}
<form action="" method="POST">{% csrf_token %} # {% csrf_token %}はセキュリティ対策となりここに書くのが一般的
{{ form.as_p}} # フォームの設定(詳細はviews.pyで設定)
<input type="submit" value="create">
</form>
{% endblock %}
  • DeleteView, UpdateView

    • CreateViewを基本とする 
  • urlタグの設定

    • reverseと同じイメージ
# list.html
{% block content %}
<div class="container">
  {% for item in object_list %}
  <div class="alert alert-{{ item.priority }}" role="alert">
    <p>{{ item.title }}</p>
    # {% url 'update' item.pk %}がポイントとなりupdate画面へ遷移する
    <a href="{% url 'update' item.pk %}" class="btn btn-info" tabindex="-1" role="button" aria-disabled="true">編集画面へ</a>
  </div>
  {% endfor %}
</div>
{% endblock %}

社内SNSアプリ

  • render
# views.py
from django.shortcuts import render

def signupfunc(request):
    # httpresponseオブジェクトの作成
    # {}はモデルの情報
    return render(request, 'signup.html', {})
  • ユーザーの作成と取得→Userモデルを使う
    • djangoのデフォルトのUserモデルを使う方法
# views.py
# ①ユーザーモデルのインポートとユーザーの取得
from django.contrib.auth.models import User

def signupfunc(request):
    object = User.objects.get(username='s') # User.objects.all()で全てのユーザーを取得
# ②migrateの実行
# models.pyを修正していないためmigrationfileの作成は不要
python manage.py migrate
# ③ユーザーの作成
python manage.py createsuperuser
  • フォームからのユーザーの作成→POSTを使う
# signup.html
# ①フォームをPOSTにする
{% extends 'base.html' %}

{% block content %}
<body class="text-center">
    
  <main class="form-signin">
    <!-- 書く場所注意 -->
    <form method="POST">{% csrf_token %}
      <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
      <div class="form-floating">
        <input type="text" class="form-control" id="floatingInput" placeholder="username" name="username">
        <label for="floatingInput">username</label>
      </div>
      <div class="form-floating">
        <input type="password" class="form-control" id="floatingPassword" placeholder="Password" name="password">
        <label for="floatingPassword">Password</label>
      </div>
      <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
      <p class="mt-5 mb-3 text-muted">&copy; 2017–2023</p>
    </form>
  </main>
</body>
{% endblock %}
# views.py
# ②ユーザーの作成
# request.POSTでフォームからのデータを取得するのがポイント
def signupfunc(request):
    # userの作成
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = User.objects.create_user(username, "", password)
  • 登録データの重複を防ぐ→try exceptを使う
# views.py
from django.db import IntegrityError

def signupfunc(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        try:
            user = User.objects.create_user(username, "", password)
        except IntegrityError:
            return render(request, 'signup.html', {"error": "このユーザーは既に登録されています"})
  • ログイン機能→authenticate, loginを使う
# ①signupと同じような画面を作成する
# login.html
# ②authenticateで認証
# views
from django.contrib.auth import authenticate, login

def loginfunc(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return render(request, 'login.html', {"context": "logged in"})
        else:
            return render(request, 'login.html', {"context": "not logged in"})
    return render(request, 'login.html', {"context": "get method"})
  • renderとredirect

    • renderは違うveiwsを呼び出さない(データを組み合わせてレスポンスを返す)
    • redirectは違うveiwsを呼び出す
    • 使い分けは複数のcontextを使う場合はrender、処理が終わり違う所に移す場合はredirectを使う
  • modelsの記載例

# models.py
from django.db import models

class BoardModel(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    author = models.CharField(max_length=50)
    # settings.pyで設定されたデフォルトの場所に保存するならブランクでよい
    sns_image = models.ImageField(upload_to="")
    good = models.IntegerField()
    read = models.IntegerField()
    readtext = models.TextField()
  • viewsの記載例
# views.py
from .models import BoardModel

def listfunc(request):
    object_list = BoardModel.objects.all()
    # キーはバリューと同じ名前が一般的
    # list.htmlに{}書きでモデルのデータを渡す
    return render(request, "list.html", {"object_list": object_list})
  • list.htmlの記載例
# list.html
{% block content %}
<div class="container">
  {% for item in object_list %} # object_listでviewsより受け取る
  <div class="alert alert-success" role="alert">
    <p>タイトル:{{ item.title }}</p> # {{}}でデータを扱う
    <p>投稿者:{{ item.author }}</p>
    <a href="#" class="btn btn-primary" tabindex="-1" role="button" aria-disabled="true">Primary link</a>
    <a href="#" class="btn btn-secondary" tabindex="-1" role="button" aria-disabled="true">Link</a>
  </div>
  {% endfor %} # ここはendfor
</div>
{% endblock %}
  • imageファイル
    • 開発時の取り扱い
    • ユーザーがアップロードした画像→MEDIAを使う
# settings.py
# 画像のURLとなり最後スラッシュを入れる
MEDIA_URL = "medi/"

# 画像の保存先(開発環境用)
MEDIA_ROOT = BASE_DIR / "media"
# (プロジェクトの)urls.py
from django.conf.urls.static import static

urlpatterns = [
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
  • モデルにイメージフィールドを追加しpillowをインストールする
# models.py
class ItemModel(models.Model):
    ...
    item_image = models.ImageField(upload_to="")
pip install pillow
  • 画像の表示方法
# list.html
<img class="card-img-top" src="{{ item.item_image.url }}" alt="{{ item.name }}" />
  • cssファイル
    • 開発者が扱う画像やCSSファイル→STATICを使う
# 画像のURLとなり最後スラッシュを入れる
STATIC_URL = 'static/'

# 画像の保存先(本番環境用となり本番環境はSTATICFILES_DIRSからここに一つにまとめる)
STATIC_ROOT = BASE_DIR / "staticfiles"

# 画像の保存先(開発環境用となり複数の保存先を指定可)
STATICFILES_DIRS = [str(BASE_DIR / "static")]
from django.conf.urls.static import static

urlpatterns = [
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
  + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
  • cssファイルをstaticフォルダに保存して読み込ませる
# base.html
<head>
    {% block customcss %}
    {% endblock %}
</head>
# signup.html
{% extends 'base.html' %}
<!-- staticfilesdirsの場所を読み込む -->
{% load static %}

{% block customcss %}
<link href="{% static 'style.css' %}" rel="stylesheet"> # staticフォルダのcssを読み込む
{% endblock %}
  • ログイン状態の判定
    • @login_requiredを使う方法
# settings.py
# ulrs.pyのname=loginを参照する(reverseのイメージ)
LOGIN_URL = "login"
# views.py
# 関数呼び出し前に処理を加える(リストの表示前にログイン画面に飛ばす)
from django.contrib.auth.decorators import login_required

@login_required
def listfunc(request):
    xxx
  • テンプレートの中にif文を書く方法
# list.html
{% if user.authenticated %}
  # タイトルや投稿者
{% else %}
please login
{% endif %}
  • ログアウト機能の実装
# ulrs.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc

urlpatterns = [
    path('logout/', logoutfunc, name="logout"),
]
# views.py
from django.contrib.auth import authenticate, login, logout

def logoutfunc(request):
    logout(request)
    return redirect("login")
# list.html
...
  {% endfor %}
  <a href="{% url 'logout' %}">logout</a>
</div>
  • DetailView
# urls.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc, detailfunc

urlpatterns = [
    path('detail/<int:pk>', detailfunc, name="detail"),
]
# views.py
from django.shortcuts import render, redirect, get_object_or_404

# 引数pkがポイント
def detailfunc(request, pk):
    # object = BoardModel.object.get(pk=pk)
    # 上記と同じくデータを取得しかつオブジェクトがなければ例外を返す
    object = get_object_or_404(BoardModel, pk=pk)
    return render(request, "detail.html", {"object": object})
# detail.html
...
    <p>タイトル:{{ object.title }}</p>
    <p>投稿者:{{ object.author }}</p>
    # 画像の表示方法
    <p><img src="{{ object.snsimage.url }}" width=300></p>
...
  • リストから詳細への飛び方
# list.html
# item.pkを渡すのがポイント
...
    <p>タイトル:<a href="{% url 'detail' item.pk %}">{{ item.title }}</a></p>
...
  • いいね機能
# urls.py
path('good/<int:pk>', goodfunc, name="good"),
# views.py
def goodfunc(request, pk):
    object = get_object_or_404(BoardModel, pk=pk)
    object.good = object.good + 1
    object.save() # ポイント
    return redirect("list")
# detail.html
<a href="{% url 'good' object.pk %}" class="btn btn-primary" tabindex="-1" role="button" aria-disabled="true">いいねする</a>

# list.html
# disabledでクリック出来ないようにする(好み)
<a href="#" class="btn btn-primary disabled" tabindex="-1" role="button" aria-disabled="true">いいね{{ item.good }}件</a>
  • 既読機能
    • 本番環境ではユーザー名の実装が弱いためNGの実装(理解のための実装)
# urls.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc, detailfunc, goodfunc, readfunc

urlpatterns = [
    path('read/<int:pk>', readfunc, name="read"),
]
# views.py
def readfunc(request, pk):
    object = get_object_or_404(BoardModel, pk=pk)
    username = request.user.get_username()
    if username in object.readtext:
        return redirect("list")
    else:
        # 既読回数を増やす
        object.read = object.read + 1
        # 既読ユーザーを入れる
        object.readtext = object.readtext + ' ' + username
        object.save()
        return redirect("list")
# detail.html
<a href="{% url 'read' object.pk %}" class="btn btn-secondary" tabindex="-1" role="button" aria-disabled="true">既読にする</a>
  • CreateView→classbasedviewを使う→ファイルの扱いが容易なため
# urls.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc, detailfunc, goodfunc, readfunc, \
                   BoardCreate

urlpatterns = [
    path('create/', BoardCreate.as_view(), name="create")
]
# views.py
from django.views.generic import CreateView
from django.urls import reverse_lazy

class BoardCreate(CreateView):
    template_name = "create.html"
    model = BoardModel
    fields = ("title", "content", "author", "snsimage")
    success_url = reverse_lazy("list")
  • @login_requiredはクラスでは指定不可→テンプレートの中にif文を書く
# create.html
{% extends "base.html" %}
{% block content %}
{% if user.is_authenticated %}
<!-- fileを扱う場合はenctypeの指定かつ複数のデータを扱う場合はmultipartを使う -->
<form method="POST" enctype="multipart/form-data">{% csrf_token %}
  <!-- {{ form.as_p }} -->
  <p>title:<input type="text" name="title"></p>
  <p>content:<input type="text" name="content"></p>
  <p><input type="file" name="snsimage"></p>
  <!-- フロント側は隠す -->
  <input type="hidden" name="author" value="{{ user.username }}">
  <input type="submit" value="create">
</form>
{% else %}
please login
{% endif %}
{% endblock %}
# models.py
from django.db import models
# 画像をアップできるようにバリデーションを外すのがポイント
class BoardModel(models.Model):
    # nullとblankを許容する(セットで設定:nullはDB関係、blanckはform関係)
    good = models.IntegerField(null=True, blank=True, default=1)
    read = models.IntegerField(null=True, blank=True, default=1)
    readtext = models.TextField(null=True, blank=True, default="a")

公式チュートリアル(Django4.0)

Udemyとの差分を主に記述する

  • テーブル間のリレーションシップの貼り方
# models.py
class Question(models.Model):
    question_text = models.CharField(max_length=200)

class Choice(models.Model):
    # リレーションシップの定義(各々のChoiceが一つのQuestionに関連付けられる)
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
# <テーブル1><テーブル2>_set.allでテーブル1に紐付くテーブル2のデータを全て取得する
question.choice_set.all
  • オブジェクトをソートして取得
# views.py
def index(request):
    # オブジェクトをソートして取得
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
# 名前空間により他のアプリと区別する
app_name = "polls"
urlpatterns = [
    path("index/", views.index, name="index"),
# index.html
{% for question in latest_question_list %}
    <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
  • index, detailなどの汎用化できるviewsはclassbasedviewを使う
  • テストはアプリフォルダの中のtests.pyに記述していく
    • テストは冗長になってもOK
  • 静的ファイル(画像、JavaScript, CSS等)はstaticフォルダに入れる
  • adminのUIを変更にするにはadmin.pyから
  • Django Debug Toolbar
python -m pip install django-debug-toolbar

Django応用

Heroku

heroku apps:create ecapp
git push heroku xxxx:main
# 強制プッシュ
git push heroku xxxx:main --force
  • アプリ消去時に再設定が必要な項目
    • Heroku上の環境変数の設定
    • GithubのOAuthに関するHerokuへのリダイレクトURLの再設定が必要
    • settings.pyのCLOUD_NAMEの設定
    CLOUDINARY_STORAGE = {
        'CLOUD_NAME': 'drsbhpxje', # アプリ消去時の再設定忘れない
        'API_KEY': env('CLOUDINARY_API_KEY'),
        'API_SECRET': env('CLOUDINARY_API_SECRET')
    }
  • DBリセット
heroku pg:reset DATABASE_URL
// heroku run python manage.py makemigrations twapp
heroku run python manage.py migrate -a twapp
  • superuserの作成
heroku run python manage.py createsuperuser

flake8

docker compose exec web flake8 --max-line-length 400

応用

# views.py
class ItemList(ListView):
    template_name = 'list.html'
    model = ItemModel
    # メソッドをオーバーライドし商品をid順に取得する
    def get_queryset(self):
        return ItemModel.objects.all().order_by('id')

class ItemDetail(DetailView):
    template_name = 'detail.html'
    model = ItemModel

    # メソッドをオーバーライドしURLから取得されるオブジェクトに加えて最新のDBデータを取得する
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['latest_item'] = ItemModel.objects.latest('id')
# views.py
def add_to_cart_from_list_func(request, pk):
    # セッションのカートを取得、なければ新しく作成
    cart = request.session.get('cart', [])
    cart.append(pk)
    # カートをセッションに保存
    request.session['cart'] = cart
    return redirect("list")
  • Djangoのテンプレートでは、変数の宣言や算術演算を直接行うことはできない。代わりに、ビューで必要な計算を行い、計算された結果をテンプレートに渡すのが一般的な方法
  • フラッシュメッセージ
  • 中間テーブル
    • カートテーブルと商品テーブルの間には多:多の関係があるため中間テーブルを設けて1:1の関係にする
    • cart_idをsessionに保存するようにする
    • 外部キーは各オブジェクトを設定、cart_itemsを各テーブルから中間テーブルへのアクセスポイントとする 
# models.py
class CartItemModel(models.Model):
    # 中間テーブルの作成
    cart = models.ForeignKey(CartModel, on_delete=models.CASCADE, related_name='cart_items')
    item = models.ForeignKey(ItemModel, on_delete=models.CASCADE, related_name='item_carts')
  • 各オブジェクトを用いて中間オブジェクトを作成、cart_itemsを用いて中間テーブルへアクセス
# views.py
# 中間オブジェクトを作成
cart_item_object= CartItemModel.objects.create(cart=cart_object, item=item_object)
cart_item_object.save()

# カートアイテムの数を取得
cart_item_count = cart_object.cart_items.all().count()
  • item.priceで中間テーブルにリンクされているリンク先のテーブルデータにアクセス
# カートアイテムを取得
cart_items = cart_object.cart_items.all()

# カートの商品数と合計金額を算定
    for cart_item in cart_items:
        total_price += cart_item.item.price
  • item__idで中間テーブルにリンクされているリンク先のテーブルデータにアクセス
# カートアイテムからpkに一致する最初のデータを削除
    cart_object.cart_items.filter(item__id=pk).first().delete()
  • DBデータの削除時はsaveは不要
# カートアイテムの削除
cart_object.cart_items.filter(item__id=pk).delete()
# cart_object.save() # 不要
  • Basic認証
  • メール送信
    • Gmailを使用
    • パスワードはGmailパスワードではなく アプリ固有のパスワードとすること
    • Code for Django
  • カスタムコマンド
  • bulk_create, bulk_updateで効率よくオブジェクトを作成・更新する
  • カスタムユーザー作成時のmigrateエラー
  • ソーシャル連携
    • GitHubのOAuth設定のherokuについてはhttps://とsを忘れない(ローカル上はhttpでよい)
  • ページネーション
    • デフォルトのPaginatorを使う
    • 公式
  • クエリパラメータについて
    • ChatGPTより:{% url %} タグでクエリパラメータを指定する場合、それはURLのパス部分ではないため、urls.pyで<int:id> のようなパスパラメータを指定する必要はありません。クエリパラメータはビュー内で request.GET からアクセスでき、URLのパス自体には影響を与えません。
  • テンプレートでのクエリパラメーターの渡し方
{% url 'twapp:tweet_good' tweet.id %}?param={{ param_value }}
  • ビューでのクエリパラメータの受け取り方
  • URLの生成はreverseを使う、idはkwargsで渡す
# クエリパラメータの受け取り
param_value = request.GET["param"]
if param_value in ["all", "follow"]:
  # URLのパス部分を生成
  url = reverse('twapp:tweet_list')
else:
  url = reverse('twapp:tweet_list_id', kwargs={'id': tweetmodel.customusermodel_id})
# クエリパラメータを追加
redirect_url = f'{url}?param={param_value}'

DBリセット

docker compose exec web python manage.py showmigrations
docker compose exec web python manage.py migrate twapp 0001_initial←戻したいファイル名
docker compose down
docker volume rm django_tw_db-data  
  • 3.makemigrationsの実行
  • 4.migrateの事項

テスト

参考文献
基礎
・株式会社CODOR (大橋亮太), 【徹底的に解説!】Djangoの基礎をマスターして、3つのアプリを作ろう!(Django2版 / 3版を同時公開中です)
公式チュートリアル
pyhaya’s diary
応用
・上記の各リンク参照

Python_sysモジュールの使い方

sysとは

  • sysとはPythonの標準ライブラリに含まれているモジュールであり、システム関連の操作を行うための機能を提供する
  • os, pathlib, input, printなどのモジュールと一部機能が被る
主要なコマンド 内容 使い方例
sys.argv コマンドライン引数へアクセス sys.argv[0]
実行ファイル名を取得
sys.exit() プログラムの終了 sys.exit("エラーが発生")
終了時のコメントが可能
sys.path 検索パスの操作 sys.path
os, pathlibと同じくパスを表示
sys.stdin 標準入力へアクセス sys.stdin.readline()
input()と同じく入力を受け取る
sys.stdout 標準出力へアクセス sys.stdout.write('Hello')
print()と同じく出力する
sys.version Pythonのバージョンへアクセス ※あまり使わない
sys.platform OSへアクセス ※あまり使わない

参考文献
2023年12月8取得/Python公式

「シリコンバレー一流プログラマーが教える Pythonプロフェッショナル大全」を読む

気になった用語等をまとめる

第1-7章_入門編

学んだこと

  • 複数行にわたる文字列の出力
print("""
1
2
3
""")

>>
←空行
1
2
3
←空行
  • 行の最後の\は次の行に続くため改行されない
  • メソッドとは変数等の後に.を付けその後に書いて実行する命令(.の前にある変数等の型と結び付けられている)
  • リストと辞書は参照渡し
  • リストの重複削除にはsetを使う
l = [0, 1, 1, 2]
a = set(l)
print(a)

>> {0, 1, 2}
# 集合になる
  • 値が入っていないことを判定するテクニック
a = 0
if a:
  print(OK)
else:
  print(NG)

>> NG
# 数値が0の場合はFalseとなるためNGとなる(0以外の数値はTrue)
  • Falseになるのは0, 空の文字列・リスト・辞書・タプル
  • inputで入力した内容は必ず文字列になる
  • rangeの数値をforループで使わない時は_を使う(単純にrange分だけ繰り返したい時)
for _ in range(10):
  • 辞書のforループはキーが取り出される
  • 辞書でバリューを取り出したい時はitems()を使う
d = {"a": 1, "b": 2}
for k, v in d.items():
  • 関数の呼び出しは後で
  • 参照渡しのものはデフォルト引数に使わない
  • 関数内関数をクロージャーと言い、外側の関数に渡した引数の値が関数内関数でも保持されている
def circle_area_func(pi):
  def circle_area(radius):
    return pi * radius * radius
  return circle_area

ca1 = circle_area_func(3.14)  # 関数内関数を実行するには外側の関数の返り値を変数に格納し実行する必要がある
print(ca1(10))

>> 314.0
  • デコレーターは関数を実行する前後に処理を加えたい時に少ないコード量で書ける
  • ジェネレーターは反復処理、returnがない、他の処理が入っても次の要素の状態を保持するため、重たい反復処理を小分けにするのに便利
  • コマンドライン引数はsys.argvで取り出す
# python app.py b
import sys

print(sys.argv)

>> [app.py, b]
  • ファイルをモジュール、ディレクトリをパッケージ
  • init.pyがあることによりパッケージとなる(v3.3以降は任意)
  • 「import パッケージ.モジュール」で読み込む
  • 「from パッケージ import モジュール」で読み込む
  • 組み込み関数はimport不要、標準ライブラリはimport必要
  • クラスのメソッドには必ず引数にselfが必要
class Preson:
  def __init__(self)  ←必ずselfが必要
  • クラス内で自身のインスタンス変数とメソッドを読み出す際は必ずselfが必要
  • 変数名に_を付けてプロパティを設定するとクラス外から読み込みは可能だが書き換えは不可となる
class AdvanceCar:
  def __init__(self, _enable_auto_run=False):
    self._enable_auto_run = enable_auto_run  ←変数名に_を付ける

  @property  ←プロパティのゲッター
  def enable_auto_run(self):
    return self._enable_auto_run  ←変数を返す

advanced_car = AdvancedCar()
print(advanced_car.enable_auto_run)  ←False(呼び出しの括弧は不要)

advanced_car.enable_auto_run = True  ←エラーとなる(書き換え不可)
-----------------------------------------
# プロパティに外から値を設定できるようにしたい場合はセッターを作る
  @enable_auto_run.setter  ←プロパティのセッター
  def enable_auto_run(self, is_enable):
    self._enable_auto_run = is_enable

advanced_car.enable_auto_run = True  ←エラーとならない(書き換え可能)
  • プロパティとセッターは特定の条件に合致した時にだけ書き換え可能にしたい時に役立つ
  • クラス外から読み込みも不可にするには変数名に__(アンダースコア二つ)を付けてプロパティのゲッターを用いる
  • 抽象クラスは継承されて使うことを前提にしたものだが、Pythonでは非推奨のためパス
  • 多重継承は左のクラスのメソッドが優先される
class PersonCarRobot(Car, Person):  ←Carが優先
  ...
  • クラス変数で異なるオブジェクト間でも同じ値を共有する
class Preson:
  kind = "human"  ←ここで定義

  def who_are_you(self):
    print(self.kind)  ←呼び出す際はインスタンス変数と同じくselfを付ける

a = Person()
a.who_are_you()
b = Preson()
b.who_are_you()
  • オブジェクトとして生成される前にメソッドにアクセスするにはクラスメソッドを使う
  • 同じような使い方でスタティックメソッドがある
  • スタティックメソッドはクラスのデータにアクセスせずクラスとの関連性が薄いもの
  • 特殊メソッド(strなど)によりオブジェクトの文字列表現、足し算、比較等ができる
  • Windowsは改行が2文字分となるためファイル書き込みや読み込みの際はnewlineを使う
  • os, pathlib, glob等でファイル操作

第8-13章_応用編

学んだこと

コンフィグとロギング

  • 標準ライブラリのconfigparser, yamlでコンフィグ(設定値のスクリプト)の設定を行う
  • 標準ライブラリのloggingでログを出力する
import logging

logging.basicConfig(level=logging.INFO)  ←ログレベルの設定
logging.info('info')  ←ログの表示
  • getLoggerによりロガーというログを使い分けるオブジェクトを作る
import logging

logger = logging.getLogger(__name__)  ←__name__を渡すのがコツ
  • 出力先の設定はハンドラーを作成してロガーに渡す
import logging

h = logging.FileHandler('logtest.log')  ←logtest.logに出力する
logger.addHandler(h)  ←ロガーにハンドラーを渡す
  • フィルタを使ってログ出力の条件を設定する→logging.Filterを継承したクラスを作成し条件設定
  • ロガーの設定ファイルを読み込んで使う→logging.config.fileConfig('logging.ini'を使う)
  • メールを送信するには標準ライブラリのemail, smtplibを使う
from email import message
import smtplib

... # SMTPサーバー名やポート番号を設定していく
  • ログをメール送信するにはSMTPHandlerを使う
import logging  ←ロガーを作成する
import logging.handlers  ←addHandlerでSMTPHandlerを指定する
...

Web

  • 辞書型をjson.dumpsに渡すことでJSON形式の文字列に変換する
  • JSON形式の文字列を辞書型にするにはjson.loadsを使う
  • JSONをファイルに書き込むにはjson.dumpを使う
  • JSONを読み込むにはjson.loadを使う(辞書型として読み込む)
import json

with open('test.json', 'w') as f:
  json.dump(j, f)

with open('test.json', 'r') as f:
  json.load(f)
import requests

payload = {'key1': 'value1'}
r = requests.get('http://~', params=payload)  ←payloadでデータを渡す

r.text  ←テキストで出力
r.json()  ←JSON形式で出力
  • Flaskの基本コード
from flask import Flask
from flask import g
from flask import render_template
from flask import request
from flask import Response


app = Flask(__name__)

# トップページ
@app.route('/')
def hello_world():
    return 'top'

# <変数>でデータを渡す
@app.route('/hello')
@app.route('/hello/<username>')
def hello_world2(username=None):
    return f'hello world!{username}'

def main():
    app.debug = True
    # サーバーの起動
    app.run()

if __name__=='__main__':
    main()
  • DBの利用
    • DBへの接続とテーブルの作成
    • GET, POST, PUT, DELETEの処理は別々に書く
    • SQLを実行するためのカーソル(curs.close())とDB接続のクローズ(@app.teardown_appcontext)
  • BeautifulSoupでタグとクラスを指定して情報を取得する(GETリクエストによるスクレイピング

並列化

  • 3つの方法
    • マルチスレッド:1つのCPUで1つのメモリを共有(作業単位であるスレッドが複数走る)
    • マルチプロセス:複数のCPUで複数のメモリを使用(作業単位であるプロセスが複数走り、データの受け渡しはプロセス間通信が必要)
    • 上記をconcurrent.fururesを使って実装:上記の切り替えが容易で単純な並列化におすすめ
  • マルチスレッド
    • スレッドに引数を渡せる
    • デーモンスレッドにより待たずに処理を進める
    • LockとRLockでスレッドの実行を制御
    • セマフォでスレッドの実行数を制御
# 基本形
import threading

def worker1():  ←worker1, 2を並列で処理
  ...

def worker2(): ←worker1, 2を並列で処理
  ...

if __name__ == '__main__':
  t1 = threading.Thread(target=worker1)  ←スレッドの作成
  t2 = threading.Thread(target=worker2)  ←スレッドの作成
  t1.start()  ←スレッドの実行
  t2.start()  ←スレッドの実行
  • マルチプロセス
    • 基本的な使い方はマルチスレッドを同じ
    • プールでプロセスの実行数を制御
    • マップでプロセスの実行数を簡単に制御
    • パイプで他のプロセスにデータを渡す
# 基本形
import multiprocessing

def worker1():  ←worker1, 2を並列で処理
  ...

def worker2(): ←worker1, 2を並列で処理
  ...

if __name__ == '__main__':
  t1 = multiprocessing.Process(target=worker1)  ←スレッドの作成
  t2 = multiprocessing.Process(target=worker2)  ←スレッドの作成
  t1.start()  ←スレッドの実行
  t2.start()  ←スレッドの実行

参考文献
酒井 潤 (著)/KADOKAWA/2022/シリコンバレー一流プログラマーが教える Pythonプロフェッショナル大全

「ChatGPT/LangChainによるチャットシステム構築[実践]入門」を読む

気になった用語等をまとめる

第1章_LLMを使ったアプリ開発

学んだこと

  • LLMとはLarge Language Model
  • チャットボット、文章の要約、QA、AIエージェント等

第2章_プロンプトエンジニアリング

学んだこと

  • プロンプトエンジニアリングガイドで紹介されている主な手法
種類 特徴
Zero-shot プロンプトに例を与えない
Few-shot プロンプトに例を与える(特定の形式で応答させるため)
Zero-shot CoT 一言(ステップバイステップなど)追加する(正確に応答させるため)

第3章_ChatGPTのAPI利用

学んだこと

  • Function calling機能
    • LLMに関数を使いたいと判断させる(Agentsに似ている)
    • 応用でJSON形式のデータを生成させる

第4章_LangChain基礎

学んだこと

  • 6つのモジュール
名称 機能
Model I/O ・Language models
・Prompts
・Output parsers
Chains
Memory
Data connection(現在:Retrieval)
Agents
Callbacks
  • 以下の3つのサブモジュールは根幹となるモジュール
  • ①Language modelsについて
    • LLMsは一つのテキスト入力に一つのテキスト出力を返す→OpenAIクラスを使用
    • Chat modelsはチャット形式の入力に応答を返す→ChatOpenAIクラスを使用
    • Callback機能でストリーミング出力が可能
  • ②Promptsについて
    • プロンプトのテンプレート化→ChatPromptTemplateを使用(Chat Completions API形式)
  • ③Output parsersについて
    • プロンプトに出力形式を埋め込む→PydanticOutputParserを使用
    • Chat Completions APIの場合はFunction calling機能を利用して出力形式を指定した方がよい
  • Chainsについて
    • LLMChainは上記①ー③をつなぐ
    • SimpleSequentialChainでChain同士を直列に繋ぐ(例えばステップバイステップで考えさせてその後要約させる)
  • Memoryについて
    • Chat Completions APIはステートレスなため会話履歴を保存するにはMemoryが便利
    • 保存先はデフォルトでメモリとなるため永続化させるためにはDBが必要
    • 会話履歴に対してプロンプトの長さ制限への対応が必須

第5章_LangChain応用

学んだこと

  • Data connectionについて
    • LLMと外部データを接続するための機能
    • その中でRAGとは入力をベクトル化しそのベクトルと近いDB・外部データをプロンプトに含める手法
  • Data connectionの手順
    • ①ドキュメントの読み込み→色々なソース元から色々なデータを読み込める
    • ②ドキュメントの変換・ベクトル化→ベクトル化にはOpenAIのEmbeddings APIを使う
    • ③ドキュメントの保存
    • ④ドキュメントの検索
    • ⑤ドキュメントを踏まえて回答させる→RetrievalQAを使う
  • Agentsについて
    • LLMがどのツールを使うか判断して動作する(例えばDBから検索するのか、外部から検索するのか)
    • 固定的な処理の流れを実現するChainsとは対照的

第6章_外部検索・履歴を踏まえた応答をするWebアプリの実装

学んだこと

  • (参考)ホストする=サーバーを立ててデプロイする
  • (参考)Cloud9はIDE(無料)、EC2は仮想サーバー(有料)
  • (参考)GitHubにプッシュする際はSSHでないと毎回ユーザー名やパスワードが求められる
  • Streamlit Community CloudにWebアプリをデプロイする

第7章_ストリーム形式で履歴を踏まえた応答をするSlackアプリの実装

学んだこと

  • アプリの構成→サーバーレスアーキテクチャとする
    • Slack Bolt for PythonとLangChainをAWS Lambdaにデプロイ・稼働
    • LLMはChat Completions API
    • 会話履歴はキャッシュとしてMomentoを利用
  • (参考)アーキテクチャとは設計思想のこと
  • (参考)サーバーレスとはサーバー構築(OS, DB, ネットワーク等)が不要なこと
  • (参考)Slack Bolt for PythonとはSlackアプリを簡単に構築するためのフレームワーク
  • (参考)AWSのEC2とLambdaはいずれもアプリを動かす基盤となるものだが、LambdaはOS等のセットアップが不要(=サーバーレス)
  • ストリーミングレスポンスの重要な仕組み
    • サーバーへのリトライ処理により重複して回答が来てしまう
    • Lazyリスナー関数でAWS LambdaのようにHTTPレスポンスを返すと処理が終了するのを抑止し単純応答を返した後も処理を継続する→実装はシンプル
  • 生成系AIの利用をUIで明示することが重要

第8章_社内文書に答えるSlackアプリの実装

学んだこと

  • 検索結果をそのまま表示するか、RAGにより要約するかは設計時に要検討
  • アプリの構成→サーバーレスアーキテクチャとする
    • 7章の実装+ベクターDBのPineconeより情報を検索して回答させる
  • LLMに「会話履歴+質問」から質問をあらためて生成させる→会話履歴を踏まえた応答が返ってくる

第9章_LLMアプリの本番リリースに向けて

学んだこと

  • 自社ガイドラインの作成→禁止事項を明確にし利用を推進する
  • 外部サービスの利用規約を守る→Chat Completions APIを呼ぶ前後にModeration APIでチェックする
  • CI/CD(特にCIのモデルの評価)→Evaluationで回答のスコアリング、LangSmithで性能監視
  • 様々なセキュリティ対策→プロンプトインジェクション、DoS攻撃
  • 個人情報保護→完璧に防ぐのは限界があるので注意書きをわかりやすい場所にする

参考文献
吉田 真吾 (著), 大嶋 勇樹 (著)/技術評論社/2023/ChatGPT/LangChainによるチャットシステム構築[実践]入門
はるすと/2023年11月24取得/ホストとは?デプロイ・リリースとの違いも初心者向けにわかりやすく解説
江戸達博/2023年11月26取得/サーバーレスとは何か? EC2とLambdaの違いも解説
@YotaHamasaki(yoo2315)/2023年11月26取得/Cloud9でインスタンスの再起動をしても繋がらない時