molpako.py

もるぱこのブログです

[Go] TCP接続でハーフクローズを行う

概要

GoでTCPロキシーを書いていたら、接続先に書き込み完了を伝えるために、ハーフクローズが必要だった。メモ書きレベルだが、まとめておく。

この記事でしないこと

  • ACKとFINなどのパケットについての説明
  • ネットワークコネクション構造体のCloseメソッドについての説明
  • 検証作業

TCPのハーフクローズ

  • TCPは全二重通信である
  • 送受信の双方向からの切断が必要
  • 片側の切断をハーフクローズという

Go net.TCPConn

Go言語でネットワークプログラミングを行う場合、net.Conn インターフェースが一般的に使用される。しかし net.Conn は標準的なネットワークコネクションであるため、全二重通信に必要なハーフクローズに関するメソッドは含まれていない。

ハーフクローズが必要な場合は、net.TCPConnやnet.UnixConn構造体をそのまま扱う必要がある。

読み込み側のクローズ

読み込みの完了後 CloseRead を実行する。接続先から読み込みができなくなる。

func (c *TCPConn) CloseRead() error

書き込み側のクローズ

書き込みの完了後 CloseWrite を実行する。接続先には書き込みができなくなる。

func (c *TCPConn) CloseWrite() error

少しソースを追っかけてみると、shutdown(socket, SHUT_WR) を呼び出されていて、それによって、socketに対する書き込み側通信が閉じられる。という感じだった。(読み取り側も同様)

まとめ

ドキュメントにもある通り基本的にはCloseで済むパターンが多いと思う。しかし、何かしらの理由で片側通信だけをクローズしたいというときはCloseReadやCloseWriteメソッドを実行しハーフクローズすることが必要。

systemd の socket activation を試す

systemd では .socket ファイルを作成することによって socket activation という機能を使えることができます。socket activation はサービスの代わりに socket がリッスンし、リクエストをサービスに渡します。socketがリッスンしているため、サービスのリスタートなどでサービスが落ちている時間もリクエストを受け、サービスが上がったら接続を渡すことができます。

試したコードは molpako/go-server-systemd に置いていて、docker-compose も用意しているのですぐ試せる状態になっていると思います。

サーバーを作る

golang で簡単なサーバーを作ります。なんとなく時刻を返すサーバーにします。

func HelloServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, time.Now().Format(time.Stamp))
    io.WriteString(w, "\n")
}

coreos/go-systemd の activation.Listeners() は systemd から渡されたファイルディスクリプタを元に net.Listener を作り []net.Listener を返します。

func main() {
    listeners, err := activation.Listeners()
    if err != nil {
        log.Fatal(err)
    }

    if len(listeners) == 0 {
        log.Fatal("Unexpected number of socket activation fds")
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/", HelloServer)
    srv := &http.Server{
        Handler: mux,
    }
    if err := srv.Serve(listeners[0]); err != http.ErrServerClosed {
        log.Fatalf("HTTP server ListenAndServe: %v", err)
    }
}

systemd

.socket ファイルにはリッスンするアドレスを記載します

# /etc/systemd/system/hello.socket

[Socket]
ListenStream=0.0.0.0:8076

[Install]
WantedBy=sockets.target

.serviceファイルには先ほどgoで作ったサーバーのバイナリを指定します

# /etc/systemd/system/hello.service

[Unit]
Description=Hello World HTTP
Requires=network.target
After=multi-user.target

[Service]
Type=simple
ExecStart=/app/go-server-systemd

[Install]
WantedBy=multi-user.target

それぞれのファイルを配置したあと socket を起動して、リクエストを飛ばしてみます。

root@5f8340a6141a:/app# systemctl start hello.socket
root@5f8340a6141a:/app# curl 127.0.0.1:8076
Dec 10 15:56:55

返ってきた^^

socket activation を試してみる

ここから本題です。実際にサービス側を落としてリクエストがどうなるか見てみます。

root@5f8340a6141a:/app# systemctl stop hello.service
Warning: Stopping hello.service, but it can still be activated by:
  hello.socket

socket はまだ生きているよというメッセージが出ました。

socketにリクエストを飛ばすと、レスポンスが返ってきました。socket がリクエストを受けたときに service を立ち上げてくれたようです。

root@5f8340a6141a:/app# curl 127.0.0.1:8076
Dec 10 16:15:49
root@5f8340a6141a:/app# systemctl is-active hello.service
active

go の graceful shutdown を組み合わせてみる。

上記だけでは、接続が残っている時に service を restart した場合に、その接続が切れてしまいます。無理矢理ですが接続を残すためにsleep処理を入れて試してみます。

func HelloServer(w http.ResponseWriter, req *http.Request) {
    time.Sleep(5 * time.Second)
    io.WriteString(w, time.Now().Format(time.Stamp))
    io.WriteString(w, "\n")
}
root@5f8340a6141a:/app# curl 127.0.0.1:8076
curl: (52) Empty reply from server   # systemctl restart hello.service

なので本題とはそれますが、ついでに go で作ったhttpサーバーに graceful shutdown を 導入してみます。

main.goでは HUP を受け取ると server.Shutdown() を実行してプログラムを終了するようにします。

// main.go
...
    idleConnsClosed := make(chan struct{})
    go func() {
        sigint := make(chan os.Signal, 1)
        signal.Notify(sigint, syscall.SIGHUP)
        s := <-sigint
        log.Printf("HTTP server Reload: %v", s)

        if err := srv.Shutdown(context.Background()); err != nil {
            log.Printf("HTTP server Shutdown: %v", err)
        }
        close(idleConnsClosed)
    }()

    if err := srv.Serve(listeners[0]); err != http.ErrServerClosed {
        log.Fatalf("HTTP server ListenAndServe: %v", err)
    }

    <-idleConnsClosed
}

.service ファイルに reload の処理を追加します

# /etc/systemd/system/hello.service

[Service]
...
ExecReload=kill -HUP $MAINPID

go の再ビルドと service を再起動して、準備完了です。

graceful shutdown 確認

リクエストを飛ばし続けるスクリプトを実行しておきます。

root@5f8340a6141a:/app# cat test.sh
#!/bin/bash

for i in {1..100}
do
    curl -s http://127.0.0.1:8076
done
root@5f8340a6141a:/app# nohup bash test.sh &
[1] 902
root@5f8340a6141a:/app# tail -f  nohup.out
Dec 10 16:44:23
Dec 10 16:44:28
Dec 10 16:44:33

service を reload することによって graceful shutdown され、接続が切れることは無くなりました。

root@5f8340a6141a:/app# tail -f  nohup.out
Dec 10 16:51:41
Dec 10 16:51:46
^C
root@5f8340a6141a:/app# systemctl reload hello.service
root@5f8340a6141a:/app# systemctl reload hello.service
root@5f8340a6141a:/app# systemctl reload hello.service
root@5f8340a6141a:/app# tail -f  nohup.out
Dec 10 16:51:41
Dec 10 16:51:46
Dec 10 16:51:51
Dec 10 16:51:56
Dec 10 16:52:01
Dec 10 16:52:06

# restart では graceful shutdown にならないので接続が切れる
root@5f8340a6141a:/app# systemctl restart hello.service
root@5f8340a6141a:/app# tail -f  nohup.out
Dec 10 16:54:07
Dec 10 16:54:12
curl: (52) Empty reply from server

以上です。

Pythonの並行処理 concurrentモジュール

@molpako です!

前回 では、multiprocessingモジュールを勉強しました。 今回は、concurrentパッケージを勉強していきます!

concurrentパッケージには、一つだけモジュールがあります。それが、並列実行のための concurrent.futures です。

concurrent.futures

concurrent.futuresは、主に二つのクラスを提供しています。

  • ThreadPoolExecutor
  • ProcessPoolExecutor

この二つのクラスは前回紹介したthreadingとmultiprocessingを呼び出していて、スレッドやプロセスのプールを使用して非同期に実行します。また、両方ともExecutorのサブクラスで同じインターフェースを実装しているので、同じメソッドを提供しています。

では、 前回と前々回で書いた処理をconcurrent.futuresを使用し実装していきます!

ThreadPoolExecutor

まずは並列に実行する関数の作成。引数によって待つ時間を変えれるようにしています。

import select
import socket

def slow_syscall(timeout=1):
    """遅いシステムコールを実行する関数"""
    select.select([socket.socket()], [], [], timeout)

ThreadPoolExecutorを使用して、slow_syscall()を並列に実行します。並列処理の終了を同期するために、withステートメントを使用しています。これは、内部で shutdown(wait=True) を呼び出していて、実行されたプール内のスレッドが全て終わるまで待機させています。

from time import time
from concurrent.futures import ThreadPoolExecutor

start = time()
# with を使うことで、pool内の実行がすべて終わるまで待つ
with ThreadPoolExecutor(max_workers=10) as pool:
    for _ in range(10):
        pool.submit(slow_syscall)

print('Took %.3f seconds' % (time() - start))

>>>
Took 1.006 seconds

ProcessPoolExecutor

関数の作成。

def factorize(number):
    """素因数分解する関数"""
    for i in range(1, number + 1):
        if number % i == 0:
            yield i

def call_factorize(number):
    """イテレーターをリストに変換する"""
    return list(factorize(number))

ThreadPoolExecutorと同じインタフェースを実装しているため、同じように扱うことができます。ついでに計算した結果も出力しておきます。

from concurrent.futures import ProcessPoolExecutor

numbers = [53541233, 21235343, 11421443, 5423123]
start = time()

with ProcessPoolExecutor(max_workers=2) as pool:
    # mapは呼び出す関数をイテラブルな要素それぞれに対して実行する。
    results = pool.map(call_factorize, numbers)

    for result in results:
        print(result)

print('Took %.3f seconds' % (time() - start))

>>>
[1, 5501, 9733, 53541233]
[1, 21235343]
[1, 11, 383, 2711, 4213, 29821, 1038313, 11421443]
[1, 5423123]
Took 4.070 seconds

まとめ

  • threadingやmultiprocessingを直接扱わずとも、concurrent.futuresを使用して並列処理ができる。
  • 両方とも簡単なインターフェースを実装していてとても扱いやすい。

次回は、データのやり取りを安全に行うための queueモジュールについて勉強しますー!

参考文献

Pythonの並行処理 multiprocessingモジュール

@molpako です!

Pythonを勉強していて並行処理あたりが難しいと感じたので、Golangと比較しながらまとめていきます。

前回 では、threadingモジュールを勉強しました。 今回は、multiprocessingを勉強していきます!

multiprocessing

multiprocessingのサンプルコードをみると start() や join() というメソッドがあるしthreadingと同じじゃん!マルチスレッドとマルチプロセスはどっちを使えばいいんだ!と感じましたが、ドキュメントを見ると答えが書いてありました。multiprocessingモジュールの目的は 並列処理 ということです。

threadingの所で説明しましたが、PythonはGILという仕組みがあって、それがスレッドを同時に一つのスレッドしか動かさないようにしています。multiprocessingはその問題を解決するモジュールらしく、名の通り複数のプロセスを使いマルチコアの恩恵を受け、並列処理ができるみたいです。早速CPUバウンドな処理を並列にして高速化してみましょう。まずは並列ではなく、順番に実行します。

def factorize(number):
    """素因数分解する関数"""
    for i in range(1, number + 1):
        if number % i == 0:
            yield i

numbers = [53541233, 21235343, 11421443, 5423123]

from time import time
start = time()
for number in numbers:
    list(factorize(number))

print('Took %.3f seconds' % (time() - start))

>>>
Took 7.344 seconds

処理時間は約7秒でした。次に、プロセスクラスを作成し並列に実行していきます。複数プロセスの終了を待機するには、Threadクラスと同じように join() を使います。

import multiprocessing

class FactorizeProcess(multiprocessing.Process):
    """計算するプロセスの各処理を表すクラス"""
    def __init__(self, number):
        super().__init__()
        self.number = number
    
    def run(self):
        self.factors = list(factorize(self.number))

# プロセスの開始
start = time()
procs = []
for number in numbers:
    proc = FactorizeProcess(number)
    proc.start()
    procs.append(proc)

for proc in procs:
    proc.join()

print('Took %.3f seconds' % (time() - start))

>>>
Took 4.885 seconds

並列実行の場合の処理時間は約4.9秒!正直パフォーマンス的にもっと早くなるものかと思っていましたが、CPUバウンドな処理でも並列実行され時間が短縮できたのが確認できました。これは、多分、おそらく、予想ですが、プロセスはスレッドより重くオーバーヘッドがありメモリ使用量も多いから?と思います。

ちなみに、multiprocessingのPoolクラスを使用すると上記よりも少ないコード量でワーカープロセスのプールを制御し複数のプロセスを並列に動かすことができます。作成した素因数分解する関数 factorize() を使用して試してみましょう。

def call_factorize(number):
    """イテレーターをリストに変換する"""
    return list(factorize(number))


start = time()

# 計算する要素分プロセスを立ち上げる
with multiprocessing.Pool(len(numbers)) as pool:
    results = pool.map(call_factorize, numbers)

    for result in results:
        print(result)

print('Took %.3f seconds' % (time() - start))

>>>
[1, 5501, 9733, 53541233]
[1, 21235343]
[1, 11, 383, 2711, 4213, 29821, 1038313, 11421443]
[1, 5423123]
Took 5.252 seconds

ちなみにgolangでは、前回と同じようにgoroutine 1 を使えば計算処理もはやくなります。

メモリの共有

2つのプロセス間でデータのやり取りをするためには、Pipeクラスを使用します。(Queueクラスもありますが、別の記事で紹介します!)2

Pipe()が返すコネクションオブジェクトは send() , recv() などのメソッドがあり、socketオブジェクトに似ていますね。早速Pipeクラスを使用して2つのプロセス噛んでデータをやり取りしてみましょう。

FactorizeProcess を少し変更して、コネクションオブジェクトを扱えるようにします。プロセスが開始されると、計算をし、結果をパイプ先のプロセスへと送信します。

class PipeFactorizeProcess(multiprocessing.Process):
    """計算するプロセスの各処理を表すクラス
    結果をパイプ先のプロセスに送信する"""

    def __init__(self, numbers, conn):
        super().__init__()
        self.numbers = numbers
        self.conn = conn

    def run(self):
        for number in self.numbers:
            self.conn.send(list(factorize(number)))
        self.conn.close()

受信用のプロセスは、5秒間データが受信できるか確認します。確認ができたら受信をして、データの受信がなければコネクションを閉じます。

# Pipe()は、パイプの両端を表すConnectionオブジェクトのペアを返す!
parent_conn, child_conn = multiprocessing.Pipe()
p = PipeFactorizeProcess(numbers, child_conn)
p.start()
while True:
    if parent_conn.poll(5):
        print('receive: {}'.format(parent_conn.recv()))
    else:
        parent_conn.close()
        break
p.join()

>>>
receive: [1, 5501, 9733, 53541233]
receive: [1, 21235343]
receive: [1, 11, 383, 2711, 4213, 29821, 1038313, 11421443]
receive: [1, 5423123]

ちなみにgolangでは、プロセスやスレッドを扱わず、goroutineを扱います。goroutine間でのデータのやり取りはチャネルの通信によってデータのやり取りを行います。

// 素因数分解する関数
func factorize(numbers []int, c chan<- []int) {
    for _, number := range numbers {
        var a []int
        for i := 1; i < number+1; i++ {
            if number%i == 0 {
                a = append(a, i)
            }
        }
        c <- a
    }
    // 送信側がチャネルをクローズする
    close(c)
}


func main() {
    numbers := []int{53541233, 21235343, 11421443, 5423123}
    c := make(chan []int)
    go factorize(numbers, c)

    for i := range c {
        fmt.Printf("receive: %d\n", i)
    }
}

>>>
receive: [1 5501 9733 53541233]
receive: [1 21235343]
receive: [1 11 383 2711 4213 29821 1038313 11421443]
receive: [1 5423123]

まとめ

  • start()やjoin()などthreadingとAPIが似ている。(ので、移行がしやすい)
  • threadingと違い、マルチコア実行ができる。
  • Poolクラスにより複数プロセスの管理が簡単になる。
  • Pipeクラスにより二つのプロセスでデータをやり取りできる。

次回は 並列性 のための councurrent モジュールについて勉強しますー!

参考文献


  1. スレッドより小さい軽量スレッド。Golangで並行・並列処理する場合には、goroutineを扱う。

  2. データをやり取りするためにメモリを共有する方法があるが、デフォルトではメモリを共有しない。プロセス同士でメモリを共有したい場合は、ValueクラスやArrayクラスもしくは multiprocessing.sharedctypes を使用する。

Pythonの並行処理 threadingモジュール

@molpako です!

Pythonを勉強していて並行処理あたりが難しいと感じたので、Golangと比較しながらまとめていきます。

並行処理で使用する標準ライブラリ

Python 並行処理 1 」などで検索するといくつかのモジュールがでます。基本的には以下が出てくると思います。

このモジュールたちの使い方、使うべきタイミングなど勉強して行きましょうー!まずは、threadingから!

threading

threadingはスレッドを扱うモジュールです。正直スレッドに対しては「プロセスより軽い物でマルチスレッドとかで並行か並列処理できるんだろう」という認識しかなく、全然具体的なイメージを持っていませんでした。Pythonは並列処理 2 は向いていないということをネットでみたりしていたので「スレッド扱えるなら並列処理できるんじゃないの」と思っていましたがその曖昧な認識が間違っていたことに勉強してやっと気づけました。「実行がマルチスレッド」=「CPUのマルチコアを活用できる」=「並列処理できる」という認識は、実際C++Javaのような言語では間違ってないみたいですが、Pythonではそうではないみたい。。。それはなぜかと言うと、PythonのGIL(グローバルインタプリタロック) 3 という仕組みが同時に一つのスレッドしか進行できないようにしているからです。

つまりPythonではマルチスレッドを使用してもマルチコアの恩恵を受けられず並列処理でスピードアップできないとのことです!スピードアップできないならいつ使うの!?と思っていたのですが、使うべきタイミングはドキュメントに以下のように書いていました。

I/Oバウンドなタスクを並行して複数走らせたい場合においては、 マルチスレッドは正しい選択肢です。

と記載されている通りI/Oバウンドタスクをスレッドで実行しプログラムから隔離することによって、ブロッキングI/O 4 の処理を行いながら必要な処理ができます。それでは、 select を使用して0.1秒のI/Oイベントを発生させる関数slow_syscall()を作成し、実験してみます。

from time import time
import select
import socket

def slow_syscall():
    """遅いシステムコールを実行する関数"""
    select.select([socket.socket()], [], [], 0.1)


# メインの実行スレッドが 1秒(=0.1 * 10) ブロックされる
start = time()
for _ in range(10):
    slow_syscall()
print('Took %.3f seconds' % (time() - start))

>>>
Took 1.024 seconds

複数のシステムコールを別々のスレッドで実行します。

start = time()
threads = []
for _ in range(10):
    thread = threading.Thread(target=slow_syscall)
    thread.start()
    threads.append(thread)

# join()で全てのスレッドの処理が終了するまで待機する
for thread in threads:
    thread.join()
print('Took %.3f seconds' % (time() - start))

>>>
Took 0.103 seconds

スレッドにブロッキングI/Oを処理させることで並列に実行され処理時間が約1/10になりました。ちなみにGolangでは、goroutineという軽量スレッドと使用し複数のgoroutineにそれぞれブロッキングI/Oの処理をさせることができます。下の例では順番に slowSyscall() を実行したので約1秒かかりました。

func slowSyscall() {
    fd, _ := syscall.Socket(
        syscall.AF_INET,
        syscall.SOCK_DGRAM,
        syscall.IPPROTO_UDP,
    )

    // 0.1秒かかるようにする
    timeout := &syscall.Timeval{Sec: 0, Usec: 100000}
    syscall.Select(fd, nil, nil, nil, timeout)
}

func Successive() int {

    // 逐次的に関数を実行させる
    for i := 0; i < 10; i++ {
        slowSyscall()
    }
    return 0
}

// --- PASS: TestSuccessive/#00 (1.02s)

slowSyscall() をそれぞれgoroutineに実行してもらうと、並列に実行され処理時間が0.1秒とpythonのthreadingと同じ結果になりました。

func Concurrency() int {
    wg := &sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        wg.Add(1)

        // goroutineを立ち上げ関数を実行させる
        go func() {
            slowSyscall()
            wg.Done()
        }()
    }

    // 複数のgoroutineの処理が終わるまで待機する
    wg.Wait()
    return 0
}
// --- PASS: TestCunccurency/#00 (0.10s)

スレッドは、メモリ空間を共有するので複数のスレッドがグローバルなオブジェクトを扱うときは危険です。ロックを使って回避します。10個のスレッドを並列に実行し、スレッドでカウンタを上げていくプログラムで、データ競合を起こしてみます。

まずは、変数とカウンタクラスを用意します。

thread_number = 10

# スレッドでカウンタをあげる回数
call_number = 10**5

class Counter(object):
    """カウントするクラス。スレッドにこのクラスのオブジェクトを渡す"""
    def __init__(self):
        self.count = 0
    
    def increment(self):
        self.count += 1

ここで、データ競合を起こしやすくするため Barrierを使用します。Barrierはブロックのような働きをしてくれて、Barrierオブジェクトを生成する時に指定された数分だけの wait() が呼び出されると、同時にブロックから解放されます。これにより以下ではスレッド立ち上げのオーバーヘッドのせいでデータ競合が発生しにくくなるのを防ぎます。

b = threading.Barrier(thread_number)

def syscall_worker(i, counter):
    """i回システムコールを実行し、その度にカウントを1あげる"""
    b.wait()
    for _ in range(i):
        # 実行しない方が競合が起きやすいので実際にシステムコールは実行しない
        # slow_syscall()
        counter.increment()


# スレッドの開始
threads = []
counter = Counter()
for _ in range(thread_number):
    thread = threading.Thread(
        target=syscall_worker, args=(call_number, counter))

    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()

print('want: {}, got: {}'.format(
    thread_number * call_number, counter.count))

>>>
want: 1000000, got: 466988

出力を見ると、カウンタの数がおかしくなっていました。これはスレッド同士が処理結果を上書きしあいデータの不整合が起きているからみたいでした。threadingではそうのようなデータ競合が起きさせないように、Lockクラスが用意されています。。インクリメントする時に、ロックをかけるようにしてみてもう一度実行してみます。

class LockCounter(object):
    def __init__(self):
        self.lock = threading.Lock()
        self.count = 0
    
    def increment(self):
        with self.lock:
            self.count += 1

# もう一度スレッドの実行をする
...
>>>
want: 1000000, got: 1000000

できました!ロックをとった分、実行時間は遅くなりましたがデータ競合は起きてなく求める値が取得されました!

まとめ

  • PythonのGILが、マルチスレッドを使ってもマルチコアの恩恵を受けれないようにしている。
  • I/Oバウンドなタスクを扱うときはthreadingモジュールを使う。
  • 複数スレッドで同じオブジェクトを扱うときは、threading.Lockクラスを使う。

次回は、multiprocessingを勉強していきますー!

参考文献


  1. 複数のタスクを 見かけ上 同じ時間に実行すること。OSが1コア上で実行するプロセスを切り替えている。他のタスクを待たせないのが目的。

  2. 見かけ上ではなく、実際に 複数のタスクを同じに時間に実行すること。複数のコアが別々の仕事を実行している。早くするのが目的。

  3. スレッド同士がデータ競合しないようにするロック機構のこと。

  4. I/O処理中は待機するようなI/Oのこと。同期I/O。

ダックタイピングって?

こんにちわ。もるぱこです。

何気なくいつものようにPythonの記事を眺めるためにネットサーフィンしていると、以下の記事で、

動的型付けで得るもの、失うもの。
最後の指摘は、動的型付けは得ることよりも失うものの方が大きい、というものです。
まず、得るものとは何か? について考えましょう。ここで僕が言えるのは、動的型付けではなくダックタイピング(注:ダック型付けと書くべきか?)をすべきということです

という記述がありました。

www.publickey1.jp

・・・ダックタイピングって???一度、オライリーの入門Python3でチラ見はしましたが、もう忘れて(そもそも理解していなかった)しまっていました。

今日はこのPythonにおけるダックタイピングについて勉強していきます。

ダックタイピングとは

まずは、Wikipediaさん曰く、

オブジェクトがあるインタフェースのすべてのメソッドを持っているならば、たとえそのクラスがそのインタフェースを宣言的に実装していなくとも、オブジェクトはそのインタフェースを実行時に実装しているとみなせる
"If it walks like a duck and quacks like a duck, it must be a duck"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである)
ダック・タイピング - Wikipedia

らしいです・・・。つまりクラスの型が一致しているかどうかではなく、オブジェクトの関数や属性が適切かどうかで同じインターフェースを実装しているかのように振舞える。ということですかね。

それでは、サンプルコードを書いていきましょう。

書いてみる

class Dog:

    def __init__(self, name):
        self.name = name

    def sound(self):
        return 'ワンッ!'


class Duck:

    def __init__(self, name):
        self.name = name

    def sound(self):
        return 'クワッ!'


def test(animal):
    """属性nameと関数soundを使用するテストコード"""

    print('{}は、{}と鳴いた'.format(animal.name, animal.sound()))

上のクラスは「犬」のクラスです。属性は、nameをもっています。関数はsoundで引数はありません。 上のクラスは「アヒル」のクラスです。属性は、nameをもっています。関数はsoundで引数はありません。

では、上記ふたつのクラスのオブジェクトをtest()に与え実行してみましょう

実行結果

>>> test(Dog('もるぱこ'))
もるぱこは、ワンッ!と鳴いた

>>> test(Duck('もるぱこ'))
もるぱこは、クワッ!と鳴いた

OK!!

DogDuckは違うクラス(型)ですが、属性や関数が一致しているため互換性が取れていますね!

最後に

  • Python(動的型付け言語)では、ダックタイピングを行うこと
  • DogDuckは違う型なのでisinstance()type()での判別は避ける
  • 標準ライブラリabcを使用すればインタフェースの実装ができる

以上です!

最後まで見ていただきありがとうございました。これからもよろしくお願いします。

Django の User モデルを拡張する

これらのサイトを見ながらしていきます。

目標

  • User モデルを拡張して Employee モデルを作成する
  • 追加するフィールドは employee_number (社員番号)
  • employee_number は主キー

方法

ふたつあるそうです。ひとつめは、プロキシモデル を使う方法。ふたつめは、1対1リンク を使う方法。

プロキシモデル

DB に新しいテーブルを作ることなく、モデルを継承できるモデル。DB スキーマに影響を与えずに既存モデルの振る舞いを変更するために使用される。

使いどき
  • DB へ追加情報を格納する必要がないとき
  • 簡単な関数の追加やクエリマネージャーの変更するとき

1対1リンク

  • 通常の Django モデル
  • プロキシモデルとは異なり、自分のテーブルを持つ
  • OneToOneFieldを介して既存モデルと1対1の関係を持つ
使いどき

既存のモデルに関する追加情報を格納したいとき

この1対1モデルはサイトユーザーに関する、認証には関連しない情報を格納することがあるため、しばしばプロファイルモデルと呼ばれます。

今回は、既存の User モデルにフィールドを追加したいので1対1リンクを使用して拡張していきましょう!

1対1リンクを使用した User モデルの拡張

この方法では、既存の User モデルに関連する追加情報を格納する新しいモデルを作成します。

Step1. models.pyに新しいモデルの定義を行う

from django.contrib.auth.models import User

class Employee(models.Model):
    employee_number = models.IntegerField('社員番号', primary_key=True)
    user = models.OneToOneField(User, on_delete=models.CASCADE)

employee_numerが目的にあった、User に追加したい情報の社員番号です。 userauth.models.Userとの1対1リンクを持っています。

Step2. create_userし、拡張した変数を設定する

from django.contrib.auth.models import Employee, User

user = User.objects.create_user('molpako', 'molpako@mail.address', 'password')
employee = Employee.objects.create(user=user, employee_number=22)

Step3. userからemployee_numberにアクセスできるか確認する

print(user.employee.employee_numer)
22

OK!!
最後に、きちんと主キーになっているかテストコードを書いてみしょう

テストケースを書く書く

今回のテストケースは

  • employee_numberはユニークキーか
  • employee_numberからUserオブジェクトをもってこれるか

です。

テストコード(sandbox/tests.py)は以下になりました。

from django.test import TestCase
from django.contrib.auth.models import User
from django.db.utils import IntegrityError
from .models import Employee


class EmployeeModelTests(TestCase):

def test_employee_number_is_unique(self):
    """
    社員番号が同じにならないこと
    """

    user01 = User.objects.create_user('molpako01', 'molopako@mail.address', 'password')
    user02 = User.objects.create_user('molpako02', 'molopako@mail.address', 'password')

    employee01 = Employee.objects.create(user=user01, employee_number=22)

    with self.assertRaises(IntegrityError):
        Employee.objects.create(user=user02, employee_number=22)

def test_get_uesr_from_employee_number(self):
    """
    社員番号からユーザーを取得する
    """

    user = User.objects.create_user('molpako', 'molopako@mail.address', 'password')
    employee = Employee.objects.create(user=user, employee_number=22)

    self.assertEqual(Employee.objects.get(employee_number=22).user, user)

それでは実行してみます!

$ python manage.py test sandbox
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.293s

OK
Destroying test database for alias 'default'...

楽しい!