PyCon mini Shizuokaでオンライン登壇しました。
先日、PyCon mini Shizuokaで「unittest.mockを使ってテストを書こう 〜モックオブジェクトを使ってより単体テストの目的に沿ったテストに〜」という題で30分の発表をさせていただきました。
この発表をすると決めた経緯
もともと、去年の10月の「みんなのPython勉強会」で登壇オファーをいただいたのが始まりでした。「せっかくのチャンスだから登壇しよう。(当時は自分の中でテスト熱が高かったので)テストについて話したい。けど、それだと広すぎるのであまり発表を聞かないモックについて話そう」というふうに決めました。
いざ、登壇してみると、参加者はプログラミング自体の初学者が多く、「よくわからなかった」という声をちょくちょく耳にしました。
想定していた層は「テストコードを書いたことがあるけれども自分のテストコードに自信がない人」だったので、当然いえば当然かもしれませんが、誰も悪くないとはいえショックでした。
振り返りのブログでは、
もしこのトークでもう1回登壇するならば とはいえ「モック」の概念の説明が不足していたのでモックの概念をもう少ししっかり説明する 本題に入る前に予めこの発表の対象となる人を伝えておく 「モック」と「スタブ」の概念も扱って、どんな観点で単体テストを書くかをより深く扱う 対話を通してでないと理解が難しいであろう質問は2-3分の質疑応答の時間で理解されなくても落ち込まない。交流会とかで詳しく話そうねというスタンスで挑む。 Chromeで登壇する で行こうと思います。
と書いていますが
これをやるには少なくとも30分は必要そうです。
30分以上話せるイベントはなかなかなありません。リベンジはそう早くはないかなあと思っていたところ、PyCon mini Shizukokaの存在を知りました。
言葉に厳しい人と惹きつける文章を書くのが得意な人の協力の元CfPを書いた結果、CfP採択していただきました。
CfPの項目のうち、HPに載るトーク概要は最終的にこのようになりました。
発表の練習は主に、今年の1月のPyhack冬合宿で行いました。
2泊3日の合宿で計3回話しました。
こんな感じでPython猛者の方々にSlackでフィードバック書いてもらうなどしました。
発表のフィードバック用のスレッド#pyhack pic.twitter.com/95Ql0GUxMM
— みずき@コーヒー駆動Python (@mizzsugar0425) 2020年1月18日
発表の仕方だけでなく、Pythonについて新しい知見を得てスライドをパワーアップさせることができました。
具体的なところでいうと、datetime.datetime型の関数型オブジェクトを引数に渡す方法やresponsesをテストで使うことはこの合宿で教えてもらいました。
datetime.datetime型の関数型オブジェクトを引数に渡す方法のスライド↓
responsesをテストで使うスライド↓
オンラインで登壇してみた所感
結論から言うと、オンラインで話すのに慣れていなくて心残りがあるのですが
短い期間で急遽オンラインカンファレンスに切り替え準備をしてくださった運営の方々には感謝してもしきれないです。ありがとうございました。
対面で誰か聞いてくれている人を見つけて安心しようとする方面で練習していたので、オンラインで一人で喋るのは緊張しました。
YoutubeやTwitterで発表を聞いてる人のコメントを見れましたが、なかったら動揺しそうだったので見ませんでした笑
接続に時間がかかってしまったため、時間内に収めるために前半がちょっと早足になってしまったのが心残りです。
でも、発表が終わった後に恐る恐るTwitterとYoutubeの反応を見るとコメントがあって安心しました。
特に、にしもつさんとnikkieさんがほぼ全てのトークに関してTwitterで随時呟いてくださっていてすごく安心しました。ありがとうございます。
ちなみに、前回の反省点に関しては下記のような結果になりました。
とはいえ「モック」の概念の説明が不足していたのでモックの概念をもう少ししっかり説明する →達成 本題に入る前に予めこの発表の対象となる人を伝えておく →達成 「モック」と「スタブ」の概念も扱って、どんな観点で単体テストを書くかをより深く扱う →「モック」と「スタブ」の概念についてはこのトークだけでは扱いきれないと判断し断念。単体テストの観点に関しては達成。 対話を通してでないと理解が難しいであろう質問は2-3分の質疑応答の時間で理解されなくても落ち込まない。交流会とかで詳しく話そうねというスタンスで挑む。→達成 Chromeで登壇する →達成
次オンラインカンファレンスで登壇するとしたら、一人で喋っても寂しく思わない練習をして挑みたいです。
また、次のPyCon mini Shizuokaが開催されるならば、登壇した次の日にさわやかハンバーグ食べて美味しいコーヒー飲みに行きたいです。
PythonとTypeScriptで学ぶGenerics初めの一歩
Twitterで Genericsの話が浮上しているのに影響されて、Generics使うと何が良いのか落とし込みました。
なお、この2つの言語にしたのは、普段自分が使うからです。
Genericsとは
「総称型」とか「汎用型」と言われます。 型定義にGenericsを使うことで文字列型や数値型など具体的な型に依存しない 抽象的かつ汎用的な関数やクラスを作ることができます。
そもそも、「generic」という単語は「汎用の」という意味があります。 そこから、「Generics」とは汎用的な何かを指しているんだなと想像できます。
Pythonでの定義の仕方
sample.py
from typing import TypeVar, Sequence T = TypeVar(’T’) # TypeVarを使ってTという名前の型だと宣言します。 def fist(l: Sequence[T]) -> T: # 渡された配列の一番最初の要素を返す関数 return l[0]
TypeScriptでの定義の仕方
sample.ts
function first<T> (l: T[]): T { return l[0] }
サンプルコードはPythonのドキュメントから。
typing --- 型ヒントのサポート — Python 3.8.2 ドキュメント
Genericsを使わないとどうなる?
最初の要素を返す関数をGenericsを使わないで書いてみましょう。
実装し始めた頃は要素が文字列の場合のみ想定されていたとします。
sample.py
from typing import Sequence def fist(l: Sequence[str]) -> str: return l[0]
Typescript
sample.ts
function first(l: string[]): string { return l[0] }
後々、数値やオブジェクトにも使いたいという要望が出ます。
from typing import Sequence from item import Item // 自作クラス def fist_str(l: Sequence[str]) -> str: return l[0] def fist_int(l: Sequence[int]) -> int: return l[0] def fist_item(l: Sequence[Item]) -> Item: return l[0]
TypeScript
import { Item } from "@/src/item" // 自作クラス function first_string(l: string[]): string { return l[0] } function first_number(l: number[]): number { return l[0] } function first_item(l: Item[]): Item { return l[0] }
これだと、型だけ異なる同じ内容の処理の関数が型の数だけ書かないといけなくなります。
色んな型で使いたい、けどAnyではなくその型じゃないといけないよと宣言したい処理を書く時にGenericsは便利だと思いました。
例えばこれだと、lが数値の配列である時に返り値が文字列でも数値でも真偽値でもなんでもOKになってしまいます。
from typing import Any, Sequence def fist_item(l: Sequence[Any]) -> Any: return l[0]
TypeScript
function first (l: any[]): any { return l[0] }
Genericsがないと汎用的なライブラリやフレームワークの実装で型の数だけ関数やクラスを書いているとだいぶ辛いそうだなと想像しました。
辛くならないようにGenericsを使いこなせるようになりたいなと思います。
【TypeScript】型定義のためだけにimportしたクラスやインターフェースがno-unused-vars扱いされて困った話
原因を知れば大したことなかったんですけど解決まで苦戦したので備忘録を残します。
linterはeslintを使っています。
Typescript 3.7 Typescript-eslint-plugin 2.3
pages/user/registration
<template> <div class="container"> <user-edit :on-submit-callback="onSubmit" /> </div> </template> <script lang="ts"> import { Vue, Component } from 'vue-property-decorator' import { AxiosResponse } from 'axios' import UserEdit from '@/components/UserEdit.vue' // フォームのコンポーネントです。 import { DraftUser, User } from '@/libs/sampleapi' @Component({ components: { UserEdit } }) class Page extends Vue { async onSubmit (draft: DraftUser) { const draftUser: DraftUser = { name: draft.name, email: draft.email, password: draft.password } const [success, response] = await (async (): Promise<[boolean, AxiosResponse<User>]> => { // AxiosResponseとUserがno-unused-varsとされます。 try { const response = await this.$sampleapi.registerUser({ draft: DraftUser }) // ユーザーを登録するAPIです。DraftUserがno-unused-varsとされます。 return [true, response] } catch (err) { return [false, err.response] } })() if (success) { this.$toast.success('ユーザーを登録しました。') // 成功した旨のトーストを表示します。 this.$router.push(`/user/${response.data.id}`) // 登録したユーザーの詳細画面に遷移します。 } else { this.$toast.error('ユーザーを登録に失敗しました。') // 失敗した旨のトーストを表示します。 // エラーの描画は省略します。 } } } export default Page </script>
APIはこんな感じです。Axiosでの通信部分はOpenAPI generatorのコマンドで作ってくれるのですが
作成してくれたaxiosの部分のソースが人間に優しくなかったのでyamlファイルだけ掲載します。
openapi.yaml
openapi: 3.0.2 info: title: SAMPLE API version: 0.0.0 paths: /auth.register_user: post: summary: ユーザーを登録します。 operationId: register_user security: - bearerAuth: [] requestBody: content: application/json: schema: type: object properties: host_id: type: integer draft_plan: $ref: '#/components/schemas/DraftUser' required: - draft responses: 201: description: ユーザーを登録しました。 content: application/json: schema: $ref: '#/components/schemas/User' 400: description: リクエストのパラメータが不当です。 content: application/json: schema: type: object properties: errors: type: array items: $ref: '#/components/schemas/ParameterError' required: - errors components: schemas: DraftUser: type: object description: 登録対象のユーザー情報 properties: name: type: string email: type: string password: type: string required: - name - email - password User: type: object description: 登録済みのユーザー properties: id: type: integer name: type: string email: type: string required: - id - name - email ParameterError: type: object properties: field: type: string code: type: string required: - field - code securitySchemes: bearerAuth: type: http scheme: bearer
Linterコマンドを実行すると
eslint --ext .js,.vue,.ts, --ignore-pattern **/*.d.ts --ignore-path .gitignore
AxiosResponseとUserとDraftUserがno-unused-varsでひっかかってしまいました…
eslintrcの内容が原因でした。
eslintrc.js
extends: [ 'plugin:nuxt/recommended’, '@nuxtjs', ], plugins: [ 'typescript’, '@typescript-eslint’, ], rules { 'typescript/no-unused-vars': 'error', … }
こちらです。
'typescript/no-unused-vars': 'error',
typescript-eslintを使っているので@typescript-eslintのno-unused-varsにすべきでした。
~~’typescript/no-unused-vars': 'error', "@typescript-eslint/no-unused-vars": ["error", { args: "none"}],
また、argsは引数に使われているかどうかのチェックに使われます。 関数の引数に使われていなくても良いのでnoneとしました。
Noneの他のオプションだと Aster-usedでは、最後に利用された後に利用される位置引数全てがチェックされます。最後に利用された引数の前の位置引数はチェックされません。
/*eslint no-unused-vars: ["error", { "args": "after-used" }]*/ // 2 errors, for the parameters after the last used parameter (bar) // "baz" is defined but never used // "qux" is defined but never used (function(foo, bar, baz, qux) { return bar; })();
Allでは定義された引数は全て使われないといけません。
そのif文、Enumにしてみませんか。
これは、Python Advent Calendar 17日目の記事です。
Python Advent Calendar 2019 - Qiita
この記事では、私が好きな標準モジュールのうちの一つenumモジュールの基本的な使い方と enumモジュールを使ってリファクタリングする例を紹介します。
enumモジュールとは
列挙型クラスをサポートします。
列挙型とは
Pythonのドキュメントにはこう書かれています。
列挙型は、一意の定数値に束縛された識別名 (メンバー) の集合です。列挙型の中でメンバーの同一性を比較でき、列挙型自身でイテレートが可能です。
https://docs.python.org/ja/3.8/library/enum.html
enum.Enumを使用した例
例えば、血液型を表すクラスがあるとします。 血液型はA, B, O, ABの4種類から成り立っています。 (より細かい分類がありますが、ここではこの4種類のみ使うとします)
一般的なクラスで表現するとこうなります。
class BloodType: Def __init__(self, type: str): self. type = type
ただ、上記の例だとA, B, O, AB以外の文字列が入ることを許可します。
ということで、__init__内で検知します。
class BloodType: Def __init__(self, type: str): if type not in (‘A’, ‘B’, ‘O’, ‘AB’): raise ValueError(‘妥当な血液型ではありません’) self. type = type
これも悪くないですが、if文を使わなくても 正しい値をとる方法があります。
血液型を列挙型クラスで表現することです。
import enum class BloodType(enum.Enum): A = 'A' B = 'B' O = 'O' AB = 'AB'
列挙型クラスの方が、if文で書かれるよりも 血液型はどんな値が入っているのか 明示的に表現できて個人的には好きです。
血液型やトランプの記号(ハートとかダイヤとか)など
どの値を使うのか決まっているものを列挙型で表現するという選択肢を知っていると
安全でわかりやすいクラスを書くのに役に立ちます。
enumモジュールの基本的な使い方
import enum class BloodType(enum.Enum): A = 'A型' B = 'B型' O = 'O型' AB = 'AB型'
>>> import app.example >>> >>> >>> app.example.BloodType.O <BloodType.O: 'O型'> >>> app.example.BloodType.O.value 'O型' >>> app.example.BloodType.O.name 'O'
ダンダーメソッドや一般的なメソッドもクラスに作れる
例えばこんな風に。
class BloodType(enum.Enum): A = 'A型' B = 'B型' O = 'O型' AB = 'AB型' def __str__(self): return self.name @classmethod def show_all(cls) -> List[str]: return list(map(lambda c: c.value, cls))
enum.auto, enum.unique, enum.IntEnum, enum.Enumの使いどころ
enum.auto
先程の血液型の例ですが、AかBかOかABかそれぞれ識別できれば良いだけで値は何でも良いとします。
そのような時に、enum.autoを使うと値をPythonが決定してくれます。
import enum class BloodType(enum.Enum): A = enum.auto() B = enum.auto() O = enum.auto() AB = enum.auto()
自動で、1, 2, 3, 4...と振ってくれます。
enum.unique
通常のEnumだと、列挙型メンバーに同じ名前をつけることは出来ませんが、同じ値に設定することはできます。
>>> import enum >>> >>> >>> class BloodType(enum.Enum): ... A = 'A' ... A = 'B' ... ... Traceback (most recent call last): File "<input>", line 1, in <module> class BloodType(enum.Enum): File "<input>", line 3, in BloodType A = 'B' File "/home/mizzsugar/.pyenv/versions/3.8.0/lib/python3.8/enum.py", line 95, in __setitem__ raise TypeError('Attempted to reuse key: %r' % key) TypeError: Attempted to reuse key: 'A'
>>> import enum >>> >>> >>> class BloodType(enum.Enum): ... A = 'A' ... B = 'A'
enum.uniqueは、valueが重複してほしくない時に使います。
>>> import enum >>> >>> >>> @enum.unique ... class BloodType(enum.Enum): ... A = 'A' ... B = 'A' ... ... Traceback (most recent call last): File "<input>", line 2, in <module> class BloodType(enum.Enum): File "/home/mizzsugar/.pyenv/versions/3.8.0/lib/python3.8/enum.py", line 860, in unique raise ValueError('duplicate values found in %r: %s' % ValueError: duplicate values found in <enum 'BloodType'>: B -> A
enum.IntEnum
enum.Enumではvalueがintやfloatなどの数値であっても、int型やfloat型のオブジェクトと比較ができません。
>>> import enum >>> class BloodType(enum.Enum): ... A = 1 ... B = 2 ... O = 3 ... AB = 4 ... ... >>> BloodType.A == 1 False
しかし、IntEnumだと比較することができます。
>>> import enum >>> class BloodType(enum.IntEnum): ... A = 1 ... B = 2 ... O = 3 ... AB = 4 ... ... >>> BloodType.A == 1 True
例えば、他の場所から取得した数値とIntEnumの値を比較または計算したい時に役に立ちます。(ex: データベースから数値を取得して比較したい時など)
enumモジュールを使ったリファクタリング
「現場で役立つシステム設計の原則」 の第2章で書かれている申請管理システムをPythonで書き直した例を使って締めたいと思います。
申請管理システムの、申請状況を管理する部分を書きます。
ざっくり説明すると、最初になにかしたらの申請があって審査が始まったらその申請のステータスは「審査中」になる、みたいなやつです。
ここでのポイントは、ステータスが「承認済み」の申請が「差し戻し中」に変わらないように、といった意図しないステータスに遷移しないようにもしないといけないところです。
申請管理を適切な英語に直す時間がなかったので日本語プログラミングです。
https://www.amazon.co.jp/dp/B073GSDBGT/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1
まず、Enum書かないで自分が考えた中で一番読みにくい例で。
def 遷移可能かチェックする(_from: str, to: str) -> bool: if _from == '審査中' and to in {'差し戻し中', '承認済み'}: return True if _from == '差し戻し中' and to in {'審査中', '終了'}: return True if _from == '承認済み' and to in {'実施中', '終了'}: return True if _from == '実施中' and to in {'終了', '中断中'}: return True if _from == '中断中' and to in {'実施中', '終了'}: return True return False
これだと、if文が煩雑で読みにくいです。
フェアじゃないのでちょっと変えます。
def 遷移可能かチェックする(_from: str, to: str) -> bool: if _from == '審査中' and to in {'差し戻し中', '承認済み'}: return True if _from == '差し戻し中' and to in {'審査中', '終了'}: return True if _from == '承認済み' and to in {'実施中', '終了'}: return True if _from == '実施中' and to in {'終了', '中断中'}: return True if _from == '中断中' and to in {'実施中', '終了'}: return True return False
先程のよりはスッキリしました。
ただ、この例だとどのような状態が存在するのかを管理するところがありません。
文字列で制御しているため、関係のない文字列が入力される可能性があります。
また、遷移可能かチェックする関数の他にも申請ステータスを使って計算や判定をする場合
文字列だと自由に書けてしまうため
関数1で書かれているステータスの種類と関数2で書かれているステータスの種類が違う…なんてことが起こりえます。
そこで、どのような種類のステータスがあるのかは、Enumクラスで管理することにします。
import dataclasses import enum from typing import ( Dict, Set, ) class 申請ステータス(enum.Enum): 審査中 = enum.auto() 差し戻し中 = enum.auto() 承認済み = enum.auto() 実施中 = enum.auto() 中断中 = enum.auto() 終了 = enum.auto() @dataclasses.dataclass class 申請ステータス遷移: allowed: Dict[申請ステータス, Set[申請ステータス]] def __init__(self) -> None: # 本当はFinalつけて再代入負不可にしたかったけど # Cannot redefine an existing name as final と言われてできませんでした。 # init=Falseにしてもできませんでした。 # allowed: Dict[申請ステータス, Set[申請ステータス]] = { ... } は # Dictがmutableであるため # mutable default <class 'dict'> for field allowed is not allowed となります。 # PythonにImmutalbeなMappingがあれば良いのですがないようです。 self.allowed = { 申請ステータス.審査中: { 申請ステータス.差し戻し中, 申請ステータス.承認済み }, 申請ステータス.差し戻し中: { 申請ステータス.審査中, 申請ステータス.終了 }, 申請ステータス.承認済み: { 申請ステータス.実施中, 申請ステータス.終了 }, 申請ステータス.実施中: { 申請ステータス.終了, 申請ステータス.中断中 }, 申請ステータス.中断中: { 申請ステータス.実施中, 申請ステータス.終了 }, } def can_transit( self, _from: 申請ステータス, to: 申請ステータス ) -> bool: allowd_states: Final = self.allowed[_from] return to in allowd_states
これだと、どのような申請ステータスがあるのかをEnumクラスで一元管理しているので
申請ステータスを利用する時はこのクラスを使うようになります。
申請ステータスの種類が増えたり変更したら
申請ステータスのEnumクラスを変更すれば良いです。
文字列だと申請ステータスを利用している関数一つ一つを確認して変更する必要がありますが、それがなくなるので変更がより安全になります。
使ったことがない方も、便利なのでぜひ使っていただけたら、と思います!
DjangoORMでウィンドウ関数を使おう
この記事はDjango Advent Calendar 2019の記事です。
Django Advent Calendar 2019 - Qiita
そして私の初のアドベントカレンダーです!
最近仕事でBigQueryで分析関数を使うことが多いので、
そのなかでもウィンドウ関数をDjangoのORMでも使えないかなと思い調べてみました。
ウィンドウ関数とは
データベース製品よっては分析関数と呼ぶところもあればウィンドウ関数と呼ぶところもあります。
この記事ではPostgreSQLを使います。PostgreSQLでは分析関数をウィンドウ関数と呼んでいるのでウィンドウ関数で統一します。
PostgreSQLの公式ドキュメントには下記のように説明されています。
ウィンドウ関数は現在の行に何らかとも関係するテーブル行の集合に渡って計算を行います。 これは集約関数により行われる計算の形式と似たようなものです。 とは言っても、通常の集約関数とは異なり、ウィンドウ関数の使用は単一出力行に行をグループ化しません。 行はそれぞれ個別の身元を維持します。 裏側では、ウィンドウ関数は問い合わせ結果による現在行だけでなく、それ以上の行にアクセスすることができます。
例えば、このようなテーブルがある場合。
テーブル定義
Column | Type | Collation | Nullable | Default ---------------+---------+-----------+----------+-------------------------------------- id | integer | | not null | nextval('salaries_id_seq'::regclass) department_id | integer | | not null | salary | integer | | not null | employee_id | integer | | not null | Indexes: "salaries_pkey" PRIMARY KEY, btree (id) "salaries_departm_f58eef_idx" btree (department_id) "salaries_employe_a009a1_idx" btree (employee_id)
通常の集約関数で部署ごとの平均給与を出すとこのようになります。
SELECT department_id, ROUND(AVG(salary), 0) as avg_salary FROM public.salaries GROUP BY department_id ORDER BY avg_salary DESC
出力結果例
department_id | avg_salary ---------------+------------ 16 | 9737374 12 | 9694656 15 | 9647917 11 | 9588957 5 | 9338559 14 | 9337349 6 | 9123184 8 | 9070103 13 | 9063918 10 | 9020534 9 | 8948148 20 | 8934615 4 | 8929108 18 | 8712230 17 | 8695489 19 | 8669903 2 | 8596131 3 | 8578168 7 | 8551880 1 | 6790198
平均給与を出すだけなら良いですが、一人ひとりの社員の給与とその社員がその部署の中で何番目に給与が高いのかも出したいとします。
出力結果例
department_id | employee_id | salary | rank ---------------+-------------+----------+------ 1 | 414 | 40000000 | 1 1 | 255 | 30000000 | 2 1 | 316 | 20000000 | 3 2 | 342 | 60000000 | 1 2 | 673 | 50000000 | 2 2 | 359 | 40000000 | 3 3 | 409 | 60000000 | 1 3 | 476 | 20000000 | 2 3 | 410 | 10000000 | 3
このような結果を出すには上記のような単純なクエリでは出力できないので工夫が必要です。
そういう要望がある時に、ウィンドウ関数を知っていると、ウィンドウ関数を使わない場合よりもシンプルなクエリで実現できるまたはクエリのコストを抑えられることが多々あります。
ウィンドウ関数を使うとこのようなクエリになります。
SELECT department_id, employee_id, salary, rank() OVER (PARTITION BY department_id ORDER BY salary DESC) FROM public.salaries ;
ウィンドウ関数なしだとこうまります。ウィンドウ関数を使ったクエリに比べてやや複雑です。
SELECT salaries.department_id, salaries.employee_id, salaries.salary, ( SELECT COUNT(*) FROM public.salaries as rank WHERE rank.department_id = salaries.department_id AND rank.salary > salaries.salary ) + 1, rank() OVER (PARTITION BY department_id ORDER BY salary DESC) FROM public.salaries ;
また、コストもレコード数が10000件のテーブルに関して
ウィンドウ関数を使ったクエリだと1024だったのに対し
ウィンドウ関数なしのクエリだと2072289でした。およそ206倍です。
rank()を説明しますと、PARTITION BYで部署ごとに分け、ORDER BYで部署の中でsalaryが多い順に順番を振り分けます。
ウィンドウ関数のPARTITION BYやORDER BYでどのようにテーブルを振り分けているかについては、下記のBigQueryのドキュメントの図1がわかりやすいです。
BigQueryなので分析関数と書いていますが、PostgreSQLのウィンドウ関数も同じ仕組みです。
DjangoORMでWindow関数を使うには
実行環境
* Python 3.8.0 * PostgreSQL 11.4 * Django 2.2.6 * psycopg2-binary 2.8.4
今回使用するモデル(テーブル定義は先程記述したテーブルと同じになります)
models.py
from django.db import models class Salary(models.Model): # 面倒がってFK貼っていませんが実運用ではemployees departmentsテーブル作ってFK貼ると思います employee_id = models.IntegerField() department_id = models.IntegerField() salary = models.IntegerField() class Meta: db_table = 'salaries' indexes = [ models.Index(fields=['employee_id']), models.Index(fields=['department_id']), ] def __str__(self): return f'{self.department_id}_{self.employee_id}'
ウィンドウ関数をORMで表現するには、annotateを使います。
先ほどのクエリをORMで表現すると、下記のようになります。
from django.db.models.functions import Rank from django.db.models import F, Window from employee.models import Salary window = { 'partition_by': [F('department_id')], 'order_by': F('salary').desc() } Salary.objects.annotate( rank=Window(expression=Rank(), **window) ).values( 'department_id', 'employee_id', 'salary', 'rank' )
F()は、モデルのフィールドを表すオブジェクトです。この例の場合、partition_byのdepartment_idとorder_byのsalaryです。
F()式のいいところは、実際にデータベースから値を取り出してPythonのメモリに格納しなくてもモデルのフィールドを参照できるところです。
https://docs.djangoproject.com/ja/2.2/ref/models/expressions/#f-expressions
annotateでウィンドウ関数でどのような項目を出力したいかしていします。引数expressionにDenseRank()を入れると、変数windowで指定したpartition byとorder byの通りにランキングされます。
この例では、rankという名前のアトリビュートにランキングを出力するように書いています。
https://docs.djangoproject.com/ja/2.2/ref/models/database-functions/#denserank
valuesで出力するアトリビュートを指定します。
valuesを指定しないと、id, department_id, employee_id, salary, rankの全てが出力されます。
Django Debug Toolbarをインストールし、
python manage.py debugsqlshell
で上記のSQLを実行すると、
ウィンドウ関数が利用されたことを確認できました!
SELECT "salaries"."id", "salaries"."employee_id", "salaries"."department_id", "salaries"."salary", RANK() OVER (PARTITION BY "salaries"."department_id" ORDER BY "salaries"."salary" DESC) AS "rank" FROM "salaries"
https://docs.djangoproject.com/ja/2.2/ref/models/expressions/#window-functions
おまけ
ウィンドウ関数を紹介しましたが、場合によってはウィンドウ関数で表現できるけれどもウィンドウ関数を使わないほうが良い場合があります。
例えば、社員の給与と一緒にその社員が所属している部署の平均給与を表示したい場合です。
下記のような出力結果を期待します。
department_id | employee_id | salary | avg_salary ---------------+-------------+---------+------------ 4 | 1707 | 8999649 | 5492152 9 | 8296 | 8999531 | 5575559 1 | 4641 | 8999048 | 5468614 17 | 222 | 8998863 | 5376142 19 | 4686 | 8997529 | 5449444 10 | 1768 | 8994513 | 5588093 17 | 7013 | 8994161 | 5376142 15 | 6940 | 8994098 | 5535015 11 | 577 | 8992925 | 5566160 10 | 8139 | 8992773 | 5588093 3 | 7718 | 8992511 | 5595843
ウィンドウ関数を使うと、下記のようなSQLになります。
① SQLクエリ
SELECT department_id, employee_id, salary, rank() OVER (PARTITION BY department_id ORDER BY salary DESC) FROM public.salaries ;
ORMクエリ
from django.db.models import F, Window, Avg from employee.models import Salary window = { 'partition_by': [F('department_id')], 'order_by': F('salary').desc() } Salary.objects.annotate( avg_salary=Window(expression=Avg('salary'), **window) ).values( 'department_id', 'employee_id', 'salary', 'avg_salary' )
ウィンドウ関数を使わないと、こうなります。
② SQLクエリ
SELECT salaries.department_id, salaries.employee_id, salaries.salary, salary_averages.avg_salary FROM public.salaries JOIN ( SELECT department_id, ROUND(AVG(salary), 0) as avg_salary FROM public.salaries GROUP BY department_id ) as salary_averages ON salaries.department_id = salary_averages.department_id ORDER BY salaries.salary DESC ;
ORMクエリは諦めました...
こういうの見つけたのですが、普通にSQL書いたほうが楽だと思ってしまいました。↓
Self join with django ORM - Stack Overflow
部署ごとの平均給与を出すクエリまではできました。給与が高い順です。
from employee.models import Salary Salary.objects.values('department_id').annotate(avg_salary=models.Avg('salary')).order_by('-salary')
10000件レコードを入れてEXPLAINでコストを比較したところ、
①のウィンドウ関数を使ったクエリだと1024になったのに対し ②のJOINで対応したクエリだと393になりました。
ウィンドウ関数は便利ですが、その時々で出力したい結果に応じて使い分けようという話でした!
次回は、xKxAxKxさんです!
よろしくお願いします。
「データアーキテクト(データ整備人)を”前向きに”考える会」参加レポート
ブログ枠はブログが書くまでが勉強会ということで書きました!
イベントページ
analytics-and-intelligence.connpass.com
「データアーキテクト(データ整備人)の概観とこれからの展望と課題」 しんゆう さん (フリーランス)
発表資料
概要
データエンジニアはデータを集めてデータレイクに入れる人。ログ、バッチ処理などをする
データエンジニアとアナリストの間にあるのは
データの抽出
- ダッシュボードなどでデータを可視化
データの整理。イレギュラーなデータへの対応や仕様変更への対応や監視や正確性の担保
抽出、集計、管理は雑用ではなく専門家として役割を確立させるべき -> しんゆうさんは「データ整備人」という風に名付けた
分析の経験がないと抽出の依頼に対して提案ができない
データ抽出はエンジニア領域に近い部分はあるものの、それが本業ではないので非エンジニアが対応すべき
「次にデータを使う人が速やかに業務に遂行できるようにデータを使いやすくする人」
感想
懇親会での話や発表を聞く限り、エンジニアやアナリストがデータ整備人を兼務していてデータ整備業は彼らの本分ではないのでモヤモヤしているという意見が多かったので、データ整備専門チームがある今の現場は恵まれていると思った
分析の経験が自分にはないので、適切な提案できるデータ整備人ではないのが痛いところ
「3社の事例から学ぶ!現場で使われるダッシュボードの作り方」 ゆずたそ さん
発表資料
概要
メルカリをサポートしている
メルカリはデータ整備人のポジションを最近作った
ダッシュボードは運用設計してちゃんと運用開始後にも見直すべし
5W1Hは焦点を小さく絞ろう。このくらい細かい粒度で↓
- Who - 経営陣(Aさん、Bさん、Cさん)
- When - 毎週水曜日の16時から
- Where - 会議室Aで
- Why - サービス利用状況を知るために
- What - 主導線UU率の推移を
- How - 議事録テンプレのURL経由で見る
社内でSlackをどのように扱っているかのダッシュボードの例ー>Slackbotでオペレータに月イチで知らせる。ちゃんと運用される仕組み作り。
「誰が」「いつ」「どこで」使うのか説明できるダッシュボードにしよう。誰かがいつか使ってくれるかもしれないのは結局使われない。
データを可視化するのはビジネスを助けるため
感想
- データの可視化によってビジネス側が問題の原因となりうる事象に気付き、ビジネスルールを改善するという事例があった。データの可視化によってビジネスが改善される良い事例だった。メディア広告の例↓
https://speakerdeck.com/yuzutas0/20191127?slide=32
5W1Hがとても細かくて驚いたけれども、そのくらい具体的で焦点が絞られていた方がちゃんと使われるかもしれない
データ整備の目的はデータが意思決定やビジネスの改善に貢献できるようにすること、というのが伝わる発表だった
「テータ整備業でぶつかった5つの課題_テータ整備人に求められる3つのスキル」 shinaro iwai さん(株式会社オプト)
発表資料
概要
データ整備業をしている
クライアントからの依頼、社内からの依頼から求められていること: 意思決定の示唆、意思決定するために必要なことのすり合わせ
雑なデータ取得依頼ー>なんのためにデータがほしいのかしつこく聞く
https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja
ノウハウが共有されなくて辛い->GitHubにクエリTipsを保存
データ整備人に必要なスキル
感想
- Google Colaboratory使ってレビューしてみたい(されてみたい)
数値の正しさの感覚やデータ理解力が自分にはまだないので、もっとビジネスを知る必要があるのが自分の課題
何のためにデータを使いたいのかをビジネス側とすり合わせることは、余計なことをしたくない自分たちのためにも、効果的にビジネスの改善をしたいビジネス側の人たちにとっても大事なこと
「サイエンス視点からのデータアーキテクト」 堀野将晴 さん (ヤフー株式会社)
発表資料
www.slideshare.net
データサイエンスではモデリング・分析のための前処理・可視化
モデリングからサービス実装までが1チーム。
大きなデータなので前処理が必須だけれども時間もCPUも使って大変。共通データが必要
たくさんの部署と関わるのでコミュニケーション能力がとても大事
ドメイン知識も大事
データ開発運用をサービス側の開発に求めるのは失敗した。目標の違いやリソースが逼迫しているため。サイエンス側と協力して開発
ログ設計のルールは整備人と実装側の認識合わせが必要
意図通りに使われないテーブル。中間テーブル作ったら大元のテーブルとジョインされた・・・。ー> 使われ方はよく確認してから作ろう
データ整備人で価値を出すには能動的に動くことと開発運用まで携わること
感想
データサイエンスに使われるデータための開発とサービス開発は根本的な目的が違うので、サービス側に任せずデータサイエンス側と開発するというのが印象的だっ た。サービス側でも開発できると思っていたので・・・
データ整備は誰もやりたがらないからこそ能動的に動くと価値があるというのもキャリアを考える上で参考になった
全体的なまとめ
データ整備業は雑用に思われがちだけれども他の役割と片手間で専門的な知識が必要な大事な役割
データ整備人に必要なスキル
- 課題抽出力 - ビジネス上の課題を知ってビジネスの改善や意思決定に貢献できるようになろう
- コミュニケーション能力 - データ取得を依頼するいろんな部署の人や外部の人と関わるため
- ドメイン知識 - データがビジネスの改善や意思決定に使われるためにはそもそもビジネスを知っている必要がある
djangoでタイムゾーンとうまく付き合う
Djangogirls Tutorialで今まで何気なく書いていた、Postモデルのpublished_dateとcreated_dateで使うdjango.utils.timezone.now。これについて疑問に思ったことがあったので調べました。
対象
1. shell上でPost.objects.get(pk=1).pulished_dateを出力するとUTC時間で出力されるけれども、テンプレートで描画したブラウザ上では日本時間でで出力されるのはなぜ?
Djangoでは、タイムゾーンサポートを有効にした場合(つまりsettings.pyでUSE_TZ=Trueとした場合、日時は
- データベースでは、タイムゾーンなしの日時(値はUTCの日時)
- modelのインスタンスのdatetimeFieldのアトリビュートでは、タイムゾーンがUTCのdatetimeオブジェクト
- テンプレートやフォームなどエンドユーザーがやり取りする層ではsettings.pyのTIME_ZONEで設定したタイムゾーンでの時間
となります。
タイムゾーンについて、Pythonでは「native」と「aware」という概念があります。
以下、ざっくり、タイムゾーンなしの時間を「native」な時間、タイムゾーンありの時間を「aware」な時間とします。
nativeとawareについては下記の記事が分かりやすいです。
【Django】native timeをaware timeに変換する方法 | エンジニアの眠れない夜
公式ドキュメントはこちらです。
datetime --- 基本的な日付型および時間型 — Python 3.8.2 ドキュメント
例えば、Djangoでこんなモデルがあるとします。
models.py
from django.conf import settings from django.db import models from django.utils import timezone class Post(models.Model): author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE ) title = models.CharField(max_length=200) text = models.TextField() created_date = models.DateTimeField(default=timezone.now) published_date = models.DateTimeField(blank=True, null=True) def publish(self): self.published_date = timezone.now() self.save() def __str__(self): return self.title class Meta: db_table = 'posts'
Djangoのshell機能で動きを確認すると・・・
>>> from django.contrib.auth.models import User >>> from blog.models import Post >>> >>> author = User.objects.create(username='dummy', email='dummy@example.com') >>> post = Post.objects.create( author=author, title='dummy', text='dummy' )
Postを日本時間で2019/10/10 19:00:00 に保存した場合、created_dateは下記のようになります。
データベース(PostgreSQL以外) 2019-10-10 10:00:00
データベース(PostgreSQL) 2019-10-10 10:00:00+00
Djangoでは、PostgreSQLだとdatetimeFieldはデータベース上ではtimestamp with time zoneとなり、タイムゾーンはUTCとなる仕様です。
その他のデータベースだとタイムゾーンなしとなります。値はUTCの日時で保存されます。
# 上記省略 >>> post.created_date datetime.datetime(2019, 10, 10, 10, 0, 0, tzinfo=<UTC>)
Djangoでは、タイムゾーンサポートを有効にした場合、datetimeオブジェクトはawareなオブジェクトとして扱われます。
内部的な処理にはタイムゾーンがUTCのdatetimeオブジェクトを使い、 エンドユーザー入出力を行うレイヤー(テンプレートやフォームなど)では現地時間でやり取りするという思想のもと、このようになっています。
なぜフォームやテンプレートだけ現地時間で内部の処理はUTCかというと、 ユーザーが複数のタイムゾーンを使用しており、ユーザに対して彼らの時計と同じ日時を表示したい場合に内部はUTCにすると柔軟に時間の変換ができ、時間を扱うオブジェクトを処理する過程で値を間違えることを防げるからです。 また、UTCだとサマータイムを適用している場合に変換ミスが起こることを防ぎます。
とはいっても複数のタイムゾーン使っていないとイメージ湧きづらいと思うので、とりあえずDjangoではタイムゾーンを使うのがデフォルトなので よほどのことがない限りタイムゾーンサポートを有効にするべしということを抑えられれば、と思います。
詳しくは↓
https://docs.djangoproject.com/ja/2.2/topics/i18n/timezones/
2. 時間を検索条件に入れたい時、どんな形で時間のオブジェクトを渡すのが良いのだろう?
タイムゾーンを有効にしている場合、 タイムゾーンに指定したdatetimeオブジェクトかUTCをタイムゾーンとしたdatetimeオブジェクトが良いと思います。 どちらかと言えばどちらでもいいというのが個人的な所感です笑
ケースバイケースですが、 DjangoのFormクラスのDatetimeFieldのcleaned_dataや、DRFのSerializerのDatetimeFieldのvalidated_dataでは 現地をタイムゾーンにしたdatetimeオブジェクトが出力されるので それらを使うならわざわざUTCに直してから使うこともないかなと思います。
forms.py
from django import forms class SearchPostForm(forms.Form): from_date = forms.DateTimeField() to_date = forms.DateTimeField()
filterで検索してみる
>>> from blog.forms import SearchPostForm >>> from blog.models import Post >>> >>> >>> form = SearchPostForm({'from_date':'2019-10-10 10:00:00', 'to_date':'2019-10-10 11:00:00'}) >>> form.is_valid() True >>> form.cleaned_data {'from_date': datetime.datetime(2019, 10, 10, 10, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>), 'to_date': datetime.datetime(2019, 10, 10, 11, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)} >>> >>> from_date = form.cleaned_data.get('from_date') >>> to_date = form.cleaned_data.get('to_date') >>> >>> Post.objects.filter(published_date__gte=from_date, published_date__lte=to_date)[0].published_date datetime.datetime(2019, 10, 10, 1, 0, tzinfo=<UTC>)
serializers.py
from rest_framework import serializers class SearchPostSerializer(serializers.Serializer): from_date = serializers.DateTimeField() to_date = serializers.DateTimeField()
filterで検索してみる
>>> from blog.serializers import SearchPostSerializer >>> from blog.models import Post >>> >>> >>> serializer = SearchPostSerializer(data={'from_date':'2019-10-10T10:00:00+09:00', 'to_date':'2019-10-10T11:00:00+09:00'}) >>> serializer.is_valid() True >>> serializer.validated_data >>> serializer.validated_data OrderedDict([('from_date', datetime.datetime(2019, 10, 10, 10, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)), ('to_date', dat etime.datetime(2019, 10, 10, 11, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>))]) >>> >>> from_date = serializer.validated_data.get('from_date') >>> to_date = serializer.validated_data.get('to_date') >>> >>> Post.objects.filter(published_date__gte=from_date, published_date__lte=to_date)[0].published_date datetime.datetime(2019, 10, 10, 1, 0, tzinfo=<UTC>)
また、nativeなオブジェクトを使うとこんなwarningが出ます。
RuntimeWarning: DateTimeField received a naive datetime * to while time zone support is active.
Awareなオブジェクトを使う設定にしているのにnativeなの使うなよってことですね。
試しにnativeなオブジェクトで使ってみたところ、UTCとして扱われるような動きをしています。 Nativeオブジェクトだと意図しない検索結果になることがあるのでお勧めしません。
3. APIではどのように出力するのが良いだろう
テンプレートに時間を表示したい場合、自動でローカルタイムに変更されて表示されます。では、APIでは?
Postモデルの一覧を返したいとします。
Post.objects.get(pk=1)
をdjango.https.JsonResponse(またはHttpResponse)を使って返すと、UTCのままになります。
views.py
from django.forms.models import model_to_dict from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.views.decorators.http import require_http_methods from blog.models import Post @require_http_methods(['GET']) def get(request, pk: int): post = get_object_or_404(Post, pk=pk) return JsonResponse( { 'post': model_to_dict(post) } )
レスポンス
{"post": {"id": 1, "author": 1, "title": "not_published", "text": "dummy", "created_date": "2019-10-10T10:00:00Z", "published_date": null}}
現地時間でエンドユーザーに使ってもらいたい場合、JSを使ってフロント側で変換するか、view関数内で変換する必要があります。
① (JavaScriptを使ってエンドユーザーに時間を表示する場合)UTC時間でAPIは返してJavaScriptに現地時間に直してもらう
Moment.jsを使うとこんな感じになります。
moment('2019-10-10T10:00:00.000Z').tz("Asia/Tokyo").format() // ブラウザ上では`2019-10-10 19:00:00`となります。
フォーマットを変更するには
moment('2019-10-10T10:00:00.000Z').tz("Asia/Tokyo").format('YYYY年MM月DD日 HH時mm分SS秒’) // ブラウザ上では` 2019年10月10日 19時00分00秒` となります。
② タイムゾーンがUTCのdatetimeオブジェクトをDjango.utils.timezone.localtimeで現地時間に変換する。
localtime()で現地をタイムゾーンにしたdatetimeオブジェクトに変換されます。
>>> from django.utils import timezone >>> >>> >>> time = timezone.now() # 2019-10-10 10:00:00 tzinfo=<UTC>とします >>> timezone.localtime(time) datetime.datetime(2019, 10, 10, 19, 00, 00, 00000, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
③ (DRFを使っている場合)Serializerに現地時間に返してもらう。
上記と同じです。Serializerのvalidated_dataをResponseに入れて返します。
serializers.py
from rest_framework import serializers from blog.models import Post class PostModelSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ('author', 'title', 'text', 'published_date')
>>> from blog.models import Post >>> from blog.serializer import PostSerializer >>> >>> >>> post = Post.objects.get(pk=2) >>> serializer = PostSerializer(post) >>> serializer.data {'author': 1, 'title': 'already_published', 'text': 'dummy', 'published_date': '2019-10-10T10:00:00+09:00'} >>> # +09:00となっているのでタイムゾーンがAsia/Tokyoの時間とわかる。
なお、SerializerのDatetimeFieldはデフォルトでISO-8601フォーマットになっています。それ以外のフォーマットで出力したい場合、DatetimeFieldの引数format
を指定してください。
例えばこんな感じで
from rest_framework import serializers serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S %Z")
Formatのドキュメント↓
https://www.django-rest-framework.org/api-guide/fields/#datetimefield-format-strings
どちらでも良いと思いますが、DRFがよしなにやってくれているので3が間違いが少なく楽かなと個人的には思います。
ただ、いろんなAPIのドキュメントやFAQを見ていると、 どのタイムゾーンの時間なのかはレスポンスに加えないと使う人がどのタイムゾーン時間なのか分からず困るAPIになるので注意しないとな、と思いました。
DropboxのAPIは全てUTCなのでタイムゾーンの情報は書いていませんね。ドキュメントにもUTC使うよ!と書いてくれているのでレスポンスにタイムゾーンの情報がなくても「ああ、なるほどね」となります。
https://www.dropbox.com/developers/documentation/http/documentation#file_requests-get
https://developer.github.com/v4/scalar/datetime/
Connpassは現地時間ですね。
ISO-8601フォーマットで2012-04-17T20:30:00+09:00
のような形で返されます。