Python実践入門を読んでPythonを体系的に学び直した①

今までpythonをちゃんと体系的に学んだことがなかったので、Python実践入門を読んで再入門した。 Pythonを使いながらも恥ずかしながら今まで知らなかったこと、今まで理解が曖昧だった部分、Python3.8から導入された新機能など新たな学びがたくさんあったので、いくつかピックアップしてみる。

for文の変数スコープについて

Pythonのfor文は変数のスコープをブロック内に限定しない。つまり各要素に代入する時に使用した変数はfor文を抜けた後、最後の値が代入された状態になっている。

# 変数hogeが未定義であることを確認
>>> hoge
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'hoge' is not defined

# for文の変数にhogeを使用
>>> for hoge in ['a', 'b', 'c']:
...     pass
...

# for文を抜けたあとは、最後の値が代入されている
>>> hoge
'c'

# 変数hogeが定義済の場合は上書きされる
>>> for hoge in [1, 2, 3]:
...     pass
...
>>> hoge
3

式の中で代入が行える:=演算子

Pythonの3.8から追加された機能で、if文やループの条件式中などで変数への値の代入が行える演算子

>>> import random
>>> def lottery(fruits):
        # 式の中で代入を行っている
...     if fruit := random.choice(fruits):
...             return fruit
...     else:
...             return 'Miss..'
...
>>> fruits = ['apple', 'orange', 'banana', None, None]

# 実行ごとに結果が変わる
>>> lottery(fruits)
'Miss..'
>>> lottery(fruits)
'orange'

ifの条件式中で代入された変数のスコープはif文のブロック内に閉じないので注意が必要。

括弧()でくくられた複数の連続する文字列は一つの文字列とみなされる

長い文字列(URLなど)の長い文字列を定義する際に便利。

>>> URL = ('https://rnakamine.net'
...        '/blog/wdpress/archive'
...        '/2020/11/29/python')
>>>
>>> URL
'https://rnakamine.net/blog/wdpress/archive/2020/11/29/python'

f-stringの{}の中に=を加えると、評価結果と同時にその変数や式を文字列で表示できる

Pythonの3.8で追加された機能でデバッグなどで変数の値を確認したい時に便利(かもしれない)。

>>> x = 30
>>> y = 50
>>>
>>> f'{x} : {y}'
'30 : 50'
# =を追加
>>> f'{x=} : {y=}'
'x=30 : y=50'

f-stringはこちらの説明がわかりやすかった

note.nkmk.me

辞書(dict)でキーが存在しない時に例外(KeyError)を発生させたくない場合は、dict.get()を使う

これを知らなかったので、if文でわざわざ分岐して頑張ってた...

>>> fruits = {'apple': 200, 'orange': 250, 'banana': 150}
# 普通にキーを指定して取り出す
>>> fruits['apple']
200
# 存在しないキーを指定
>>> fruits['lemon']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'lemon'

# get()を使うとキーが存在しなくてもエラーにならない
>>> fruits.get('apple')
200
# キーがない場合のデフォルト値はNone
>>> fruits.get('lemon')
# デフォルト値を指定することもできる
>>> fruits.get('lemon', 'ありません')
'ありません'
>>>

デフォルト引数値がある関数は定義時に引数の式が評価される

関数を呼び出した時間を表示させたい関数があった時に

>>> from datetime import datetime
>>> def print_page(content, timestamp=datetime.now()):
...     print(content)
...     print(timestamp)
...
>>> print_page('my content 1')
my content 1
2020-11-29 16:57:59.306168
>>> print_page('my content 2')
my content 2
2020-11-29 16:57:59.306168

上記のようにデフォルト値でdatetime.now()とかやってしまうと、 表示される結果は同じ時刻になってしまう。

デフォルト引数値は関数定義時に評価されるため、デフォルト引数の式は関数が定義される時に一度だけ評価される。 なので、呼び出す時には計算済の値が毎回出力されてしまう。

期待通りの動きをさせたい場合は以下のようにする。

>>> def print_page(content, timestamp=None):
...     if timestamp is None:
...             timestamp = datetime.now()
...     print(content)
...     print(timestamp)
...
>>> print_page('my content 1')
my content 1
2020-11-29 17:03:24.791031
>>> print_page('my content 2')
my content 2
2020-11-29 17:04:00.716482

参考文献

nginxをソースコードからコンパイルしてインストールする(centos7.1)

nginxにとあるサードパーティー製のモジュールを追加して使う場面があり、nginxの場合ビルド時にそのモジュールを組み込む必要があるらしく、ソースコードからコンパイルしてインストールした時の方法をメモ。

環境

Vagrantを使用して仮想マシンの作成

今回は以下のboxを使用します。

CentOS 7 x64 (Minimal, Shrinked, Guest Additions 4.3.26) (Monthly updates)

f:id:rnakamine:20201103202956p:plain boxの追加

$ vagrant box add centos7.1 https://github.com/holms/vagrant-centos7-box/releases/download/7.1.1503.001/CentOS-7.1.1503-x86_64-netboot.box

boxが追加されているか確認

$ vagrant box list
centos7.1          (virtualbox, 0)

初期化を行いVagrantファイルを作成

$ vagrant init centos7.1

生成されたVagratnfileを以下のように記述

ホストから仮想マシンにアクセスできるようにIPアドレスの設定を行います。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "centos7.1"
  config.vm.network "private_network", ip: "192.168.33.10" # コメントアウトを外す
end

vagrant upコマンドを実行し仮想マシンを起動

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'centos7.1'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: compile-nginx-from-source-example_default_1604539606444_32114
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
    default: Adapter 2: hostonly
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
    default: The guest additions on this VM do not match the installed version of
    default: VirtualBox! In most cases this is fine, but in rare cases it can
    default: prevent things such as shared folders from working properly. If you see
    default: shared folder errors, please make sure the guest additions within the
    default: virtual machine match the version of VirtualBox you have installed on
    default: your host and reload your VM.
    default:
    default: Guest Additions Version: 5.0.0
    default: VirtualBox Version: 6.1
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
    default: /vagrant => /path/to/compile-nginx-from-source-example

sshでログインする

$ vagrant ssh
Welcome to your Vagrant-built virtual machine.
[vagrant@localhost ~]$

これで環境は構築できたので、この上でnginxをインストールしていきます。

nginxのインストール

wgetを使ってnginxのソースコードをダウンロードします。 nginxの公式サイトからダウンロードできます。

今回は現時点で最新である1.19.4を使用します。

[vagrant@localhost ~]$ cd /usr/local/src
[vagrant@localhost src]$ sudo wget https://nginx.org/download/nginx-1.19.4.tar.gz
[vagrant@localhost src]$ sudo tar xvzf nginx-1.19.4.tar.gz

次にconfigureというスクリプトファイルを実行させて、nginxのいろいろな設定やMakefile生成したりするのですが、 この段階でコンパイルフラグを使用し様々な設定やモジュールの有効化/無効化を行ったりすることができます。

今回はコンパイルオブションを指定せずにインストールしていくのですが、各種フラグについてはこちらを参考にしてください。

nginx.org

[vagrant@localhost src]$ cd nginx-1.19.4
[vagrant@localhost nginx-1.19.4]$ ./configure
checking for OS
 + Linux 3.10.0-229.el7.x86_64 x86_64
checking for C compiler ... found
 + using GNU C compiler
 + gcc version: 4.8.3 20140911 (Red Hat 4.8.3-9) (GCC)

・・・(省略)

Configuration summary
  + using system PCRE library
  + OpenSSL library is not used
  + using system zlib library

  nginx path prefix: "/usr/local/nginx"
  nginx binary file: "/usr/local/nginx/sbin/nginx"
  nginx modules path: "/usr/local/nginx/modules"
  nginx configuration prefix: "/usr/local/nginx/conf"
  nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
  nginx pid file: "/usr/local/nginx/logs/nginx.pid"
  nginx error log file: "/usr/local/nginx/logs/error.log"
  nginx http access log file: "/usr/local/nginx/logs/access.log"
  nginx http client request body temporary files: "client_body_temp"
  nginx http proxy temporary files: "proxy_temp"
  nginx http fastcgi temporary files: "fastcgi_temp"
  nginx http uwsgi temporary files: "uwsgi_temp"
  nginx http scgi temporary files: "scgi_temp"

./configureの実行後、最後の方に各種設定情報が出力されます。

あとは、make&make installコンパイルしてインストールしていきます。

[vagrant@localhost nginx-1.19.4]$ make
[vagrant@localhost nginx-1.19.4]$ sudo make install

これでインストールは完了です。

systemdにサービスを登録

Linuxの起動処理やシステム管理を行うsystemdの起動スクリプトを作成してsystemctl start nginxみたいな感じで起動できるようにします。

[vagrant@localhost ~]$ sudo vi /usr/lib/systemd/system/nginx.service

起動スクリプトを以下のように記述します。

[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

systemctlコマンドを使用してnginxを起動させます。

[vagrant@localhost ~]$ sudo systemctl start nginx
[vagrant@localhost ~]$ sudo systemctl enable nginx
ln -s '/usr/lib/systemd/system/nginx.service' '/etc/systemd/system/multi-user.target.wants/nginx.service'
[vagrant@localhost ~]$ sudo systemctl status nginx
nginx.service - nginx - high performance web server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled)
   Active: active (running) since 日 2020-11-08 00:00:08 UTC; 4min 32s ago
     Docs: http://nginx.org/en/docs/
 Main PID: 2727 (nginx)
   CGroup: /system.slice/nginx.service
           ├─2727 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
           └─2728 nginx: worker process

1108 00:00:08 localhost.localdomain nginx[2723]: nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
1108 00:00:08 localhost.localdomain nginx[2723]: nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
1108 00:00:08 localhost.localdomain systemd[1]: Failed to read PID from file /usr/local/nginx/logs/nginx.pid: Invalid argument
1108 00:00:08 localhost.localdomain systemd[1]: Started nginx - high performance web server.

これでnginxを起動するところまでできました。 次はホスト側からアクセスできるようにします。

ホスト側からアクセスできるようにする

firewalldでhttpが許可されていないので、firewall-cmdコマンドを使用してhttpを許可

[vagrant@localhost ~]$ sudo firewall-cmd --add-service=http --permanent
success
[vagrant@localhost ~]$ sudo firewall-cmd --reload
success

ホスト側から先ほどVagrantfileで設定した仮想マシンIPアドレスにアクセスすると無事アクセスできました。 f:id:rnakamine:20201108110401p:plain

参考

qiita.com

dev.classmethod.jp

qiita.com

PyPI以外からパッケージをpip installする

最近は仕事の中での運用が楽になるようなpython製の自作ツールを作ってPyPIとかで配布して手軽に使えるようにしたいという気持ちが高まり外部パッケージの配布方法など調べていると、今更ながらPyPI以外(ローカルやGitHub)から直接パッケージをインストールできることを知ったのでメモ。これだと社内でパッケージを配布する時にも使えそう。

環境

今回はこちらのパッケージを対象に様々なパッケージのインストール方法を行っていく。 github.com

PyPIからインストール

特に何も指定せずにpip installするとPyPI(The Python Package Index)からパッケージがダウンロードされる。

$ pip install sampleproject
Collecting sampleproject
  Using cached sampleproject-2.0.0-py3-none-any.whl (4.2 kB)
Collecting peppercorn
  Using cached peppercorn-0.6-py3-none-any.whl (4.8 kB)
Installing collected packages: peppercorn, sampleproject
Successfully installed peppercorn-0.6 sampleproject-2.0.0

pip listでインストール済のパッケージ一覧(管理パッケージも含め)を表示させることができる。

$ pip list
Package       Version
------------- -------
peppercorn    0.6
pip           20.2.4
sampleproject 2.0.0
setuptools    47.1.0

note.nkmk.me

ちなみにパッケージのアンインストールはpip uninstall <Package Name>で行う。

$ pip uninstall sampleproject
Found existing installation: sampleproject 2.0.0
Uninstalling sampleproject-2.0.0:
  Would remove:
    /path/to/venv/bin/sample
    /path/to/venv/lib/python3.8/site-packages/sample/*
    /path/to/venv/lib/python3.8/site-packages/sampleproject-2.0.0.dist-info/*
    /path/to/venv/my_data/data_file
Proceed (y/n)? y
  Successfully uninstalled sampleproject-2.0.0

ソースリポジトリからインストール

<Version Control System>+<Protocol>://<Repository URL>/#egg=<Package Name>の形でソースリポジトリからインストールできる。

Version Control Systemは現状でGit、SubversionMercurial、Bazaarに対応している。

$ pip install git+https://github.com/pypa/sampleproject#egg=sampleproject
Collecting sampleproject
  Cloning https://github.com/pypa/sampleproject to /private/var/folders/z9/54__8rtd2vj7z7z1d27ckcbm0000gn/T/pip-install-t1zs55fp/sampleproject
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Requirement already satisfied: peppercorn in ./venv/lib/python3.8/site-packages (from sampleproject) (0.6)
Building wheels for collected packages: sampleproject
  Building wheel for sampleproject (PEP 517) ... done
  Created wheel for sampleproject: filename=sampleproject-2.0.0-py3-none-any.whl size=4208 sha256=39820d5c160094d41e103e2aeec4d53af0f56e5fdcfab5752d6b76a6249940ae
  Stored in directory: /private/var/folders/z9/54__8rtd2vj7z7z1d27ckcbm0000gn/T/pip-ephem-wheel-cache-hev_ebop/wheels/0d/aa/5e/4788bdead4e57aa3d564a34752934c4fdac2637c2741013624
Successfully built sampleproject
Installing collected packages: sampleproject
Successfully installed sampleproject-2.0.0

ローカルにあるパッケージをインストール

まずはgit cloneリポジトリにあるパッケージをローカルに落とす。

$ git clone https://github.com/pypa/sampleproject.git
Cloning into 'sampleproject'...
remote: Enumerating objects: 490, done.
remote: Total 490 (delta 0), reused 0 (delta 0), pack-reused 490
Receiving objects: 100% (490/490), 121.91 KiB | 312.00 KiB/s, done.
Resolving deltas: 100% (243/243), done.

$ cd sampleproject

カレントディレクトリからインストールする場合は.(ドット)で指定するだけでOK。

-eオプションを使用することで、Editableモードを有効化し、コードの変更が即時に反映されるようになる。

$ pip install -e .
Obtaining file://path/to/sampleproject
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Requirement already satisfied: peppercorn in /path/to/python-plactice/venv/lib/python3.8/site-packages (from sampleproject==2.0.0) (0.6)
Installing collected packages: sampleproject
  Running setup.py develop for sampleproject
Successfully installed sampleproject
(venv) ~/Development/sandbox/python-plactice/sampleproject (master)
$ pip install
ERROR: You must give at least one requirement to install (see "pip help install")

$ pip list
Package       Version Location
------------- ------- ----------------------------------------------------------------------
peppercorn    0.6
pip           20.2.4
sampleproject 2.0.0   /path/to/sampleproject/src
setuptools    47.1.0

参考

https://amzn.to/3oxsJwzamzn.to

Azure SDK for PythonのBlob Storage Clientを使ってpandasのDataFrameをcsv形式でBlobストレージにアップロードする

pandasのDataFrameをCSV形式でBlob Storageにアップロードする場面が結構あったので、こちらの記事を参考にさせてもらいました。

sinyblog.com

こちらもAzure SDK for PythonのBlob Storage Clientを使っているようだったのですが、バージョンがv2.1を使用していたので、今回はv12.0の方を使用してBlob StorageにpandasのDataFrameのデータをアップロードしていきます。

環境

  • python 3.8.1
  • pandas 1.1.3
  • azure-storage-blob 12.5.0

サンプルデータの作成

適当にフルーツの値段を並べたようなDataframeを作成します。

import pandas as pd

df = pd.DataFrame(
    {'fruits': ['banana', 'orange', 'apple'], 'price': [100, 150, 170]})
print(df)
#    fruits  price
# 0  banana    100
# 1  orange    150
# 2   apple    170

blob storgeの作成方法は今回は割愛します。こちらのドキュメントを参考にし、ストレージアカウントを用意してください。

docs.microsoft.com

docs.microsoft.com

pandasのDataFrameをCSV形式でBlob Storageにアップロード

まずはAzure Blob Storage Clientのライブラリパッケージをpip経由でダウンロード

$ pip install azure-storage-blob

次に、ストレージアカウントの接続文字列を取得します。接続文字列はAzureのポータル上などで取得することができます。 f:id:rnakamine:20201013144754p:plain 今回は取得した接続文字列を環境変数にセットしていきます。

$ export AZURE_STORAGE_CONNECTION_STRING="<接続文字列>"

os.getenv()メソッドを使用して、先ほど環境変数に格納した接続文字列をプログラム側で取得し、 BlobServiceClientクラスのインスタンスを作成します。from_connection_string()クラスメソッドを使用して、ストレージアカウントの接続文字列を指定することができます。

import os

import pandas as pd
from azure.storage.blob import BlobServiceClient

connect_str = os.getenv('AZURE_STORAGE_CONNECTION_STRING')
blob_service_client = BlobServiceClient.from_connection_string()

get_blob_client()メソッドを使用して、BlobClientオブジェクトの参照を取得します。 引数containerで操作したいblob storageのコンテナを指定、引数blobでblobの名前を指定します。ここではsample.csvとしてCSVファイルをアップロードしたいので、下記のように指定します。

blob_client = blob_service_client.get_blob_client(container='sample', blob='sample.csv')

最後に先ほど作成したpandasのDataframeをCSV形式に変換し、upload_blob()メソッドを使用して、ファイルをアップロードします。

output = df.to_csv(index=False)
blob_client.upload_blob(output)

最終的なサンプルコードはこちらです。

github.com

上記のプログラムを実行し、Blob StorageにCSVファイルがアップロードされているのを確認できました。 f:id:rnakamine:20201011172407p:plain

ちなみに、アップデートはできないみたいなので同じblob名をuploadしようとすると

azure.core.exceptions.ResourceExistsError: The specified blob already exists.
RequestId:27a9d8cd-f01e-0019-64a9-9fac5b000000
Time:2020-10-11T08:32:08.5781790Z
ErrorCode:BlobAlreadyExists
Error:None

といったエラーが返ってきます。

Blob Storage Clientのv2.1だと、create_blob_from_text()メソッドなどを使用してuploadするのですが、 こちらはblobがあればupdate、無ければcreateみたいな動きをしているので、v12.0で同じことをしようとすると少し工夫が必要となりそうです。

参考

docs.microsoft.com

Azure Functions(Python)従量課金プランがいつのまにか東日本リージョンに対応してたので、Terraformでプロビジョニングしてみる

待望にしていたAzure Functions(Python)の従量課金プランの東日本リージョン対応ですが

qiita.com

どうせまだ使えないよなーとか思いつつAzureのポータル上から確認してみると、いつの間にか選択できるようになっていました🎉

f:id:rnakamine:20201005212331p:plain

f:id:rnakamine:20201005212446p:plain

個人的にもそろそろAzure FunctionsをTerraformでコード化しておきたいという思いがあったので、今回Terraformを使ってプロビジョニングしてみました。

www.terraform.io

環境

  • Terraform v0.12.26
  • Terraform Azureプロバイダー v2.30.0

tfファイルの作成

Azure Functions(Function App)の作成に伴い以下のリソースも一緒に作成していきます。

  • リソースグループ
  • ストレージアカウント
  • App Service Plan(従量課金プラン)
  • Application Insights

Azureプロバイダーの設定

プロバイダーのバージョンは現時点での最新版であるv2.30.0を使用します。

github.com

provider "azurerm" {
  version = "=2.30.0"
  features {}
}

最後の方でも記載しますが、Azureプロバイダのバージョンが古いとterraform apply時に失敗するので注意が必要です。

リソースグループの作成

リージョンは東日本を指定します。

resource "azurerm_resource_group" "sample" {
  name     = "sample-rg"
  location = "japaneast"
}

ストレージアカウントの作成

Azure Functionsではトリガーの管理や関数実行のログ記録などの操作にAzure Storageが必要なため、ストレージアカウントも一緒に作成していきます。

resource "azurerm_storage_account" "sample" {
  name                     = "samplestoragernakamine"
  resource_group_name      = azurerm_resource_group.sample.name
  location                 = azurerm_resource_group.sample.location
  account_tier             = "standard"
  account_replication_type = "LRS"
}

Azure Functionsで使用する場合、Blob専用ストレージ、Azure Premium Storage、ZRSレプリケーション(ゾーン冗長ストレージ)を使用するストレージはサポートされないようです。

Application Insightsの作成

resource "azurerm_application_insights" "sample" {
  name                = "sample-application-insights-rnakamine"
  location            = azurerm_resource_group.sample.location
  resource_group_name = azurerm_resource_group.sample.name
  application_type    = "web"
}

Azure Functionsの作成

resource "azurerm_app_service_plan" "sample" {
  name                = "sample-app-service-plan"
  location            = azurerm_resource_group.sample.location
  resource_group_name = azurerm_resource_group.sample.name
  kind                = "FunctionApp"
  reserved            = true

  sku {
    size = "Y1"
    tier = "Dynamic"
  }
}

resource "azurerm_function_app" "sample" {
  name                       = "sample-function-app-rnakamine"
  location                   = azurerm_resource_group.sample.location
  resource_group_name        = azurerm_resource_group.sample.name
  app_service_plan_id        = azurerm_app_service_plan.sample.id
  storage_account_name       = azurerm_storage_account.sample.name
  storage_account_access_key = azurerm_storage_account.sample.primary_access_key
  os_type                    = "linux"
  version                    = "~3"
  enable_builtin_logging     = false

  app_settings = {
    FUNCTIONS_WORKER_RUNTIME       = "python"
    APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.sample.instrumentation_key
    AzureWebJobsStorage            = azurerm_storage_account.sample.primary_connection_string
  }
}

リソースの作成

terraform initコマンドで初期化を行って、terraform plan/ terraform applyを行いリソースを作成します。

Azureのポータル上から確認すると

f:id:rnakamine:20201007185946p:plain

きちんとリソースが作成できているのを確認できます。

東日本リージョンでAzure Functions(Python)を従量課金プランで使えるようになりました🎉

Azureプロバイダのバージョンによっては上手くいかない

App Service Planを構成する際、Terraformのドキュメントにもある通り従量課金プランのAzure Functionsの場合kindFaunctionAppreservedtrueに指定する必要があります。

  • kind - 作成するApp Service Planの種類(Windows / Linux / elastic / FunctionApp)
  • reserved - コンピューティングリソースがLinuxであるかWindowsであるかを制御するフィールド。Linuxであればtrue、それ以外(Windows)であればfalseを指定。(デフォルトではfalse

しかし、Azureのプロバイダのバージョンがv2.20.0以下だとkindLinux以外だとreservedtrueに指定した場合、terraform apply時にエラーが起きます。

Error: `reserved` has to be set to false when kind isn't set to `Linux`

  on function.tf line 8, in resource "azurerm_app_service_plan" "sample":
   8: resource "azurerm_app_service_plan" "sample" {

この問題は以下のisuueで解決されており

github.com

v2.21.0でこちらの修正が取り込まれているので、それ以上のバージョンだと問題なくリソースを作成することができました。

Pythonのpandasを使ってSQL Server上のデータの取得・挿入を行う

よくcsvファイル中身をSQL Serverに挿入したりすることがあるが、普段はpythoncsvモジュールを使ってcsvファイルを読み込み、dictに変換してSQL ServerにINSERTしたりしていた。(もっといい方法があるかも知れないが...)

恥ずかしいことに、今更ながらpandasを使ったcsvファイル扱いがめちゃくちゃ楽ということに気づいたのでメモ。データ分析がっつりやる人にとっては当たり前なんだろうな...

pandasとは

pandasとは、データ分析を支援するライブラリの一つで、表データや行列を扱うことができる。 またSQLに似た操作関数が用意されているので、SQLを触ったことあるとわりととっつきやすいかも。

pandas.pydata.org

今回はpandasを使ったDataFrameの操作、SQL Serverとのデータのやりとりを書いていきたいと思う。

pandasを使ってDataFrameにcsvデータを挿入

DataFrameとは

2次元のデータ構造を表すpandasのオブジェクト。行、列の名前を持すことができたり、表に対していろいろな操作(追加、削除、抽出、ソート、etc...)などを行うことができる。

まずはpandasをpip経由でインストール

$ pip install pandas

サンプルとなるcsvファイルを用意する(sample.csv

name,price,quantity
apple,100,3
strawberry,110,5
banana,90,2
grape,150,6
orange,200,3
peach,130,2
lemon,180,4

pd.read_csv()メソッドでcsvの中身を読み込んで、DataFrameに挿入

import pandas as pd

df = pd.read_csv('sample.csv')
print(df)

これでプログラムを実行させると以下のようにDataFrameの中身を表示することができる。

$ python sample.py
         name  price  quantity
0       apple    100         3
1  strawberry    110         5
2      banana     90         2
3       grape    150         6
4      orange    200         3
5       peach    130         2
6       lemon    180         4

csvファイルを読み込む際のさまざまなオプションについてはこちらがかなり参考になった。

note.nkmk.me

このDataFrameオブジェクトに対して、条件付きで指定した行だけを抽出したり、ソートしたりといろいろできる。

例えばpriceの値を元に昇順にソートしたい場合に

import pandas as pd

df = pd.read_csv('sample.csv')
print(df.sort_values(by='price'))

sort_values()メソッドを使うと以下のように行を並び替えることができる。

$ python sample.py
         name  price  quantity
2      banana     90         2
0       apple    100         3
1  strawberry    110         5
5       peach    130         2
3       grape    150         6
6       lemon    180         4
4      orange    200         3

SQL ServerのテーブルからpandasのDataFrameにデータを挿入

今回SQL ServerはAzure SQL Databaseを使ってテーブル内のデータをpandasのDataFrameに挿入していく。

サンプルとして以下のようなテーブルをデータベースに作成し、サンプルデータをINSERT

CREATE TABLE [dbo].[sample] (
    [name] VARCHAR(20) NOT NULL,
    [price] INT NOT NULL,
    [quantity] INT NOT NULL,
)

INSERT INTO
    [dbo].[sample] (name, price, quantity)
VALUES 
    ('apple', 100, 3),
    ('strawberry', 110, 5),
    ('banana', 90, 2),
    ('grape', 150, 6),
    ('orange', 200, 3),
    ('peach', 130, 2),
    ('lemon', 180,4)

SQL Serverに接続するためにpythonODBC接続を行うことができるpyodbcモジュールをpip経由でインストール

pip install pyodbc

pd.read_sql()メソッドを使用し、SELECT文に対する結果をDataFrameに挿入

import pyodbc
import pandas as pd

server = '<ホスト名>' 
database = '<データベース名>'
username = '<ユーザー名>'
password = '<パスワード>'
cnxn = pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};SERVER='+server+';DATABASE='+database+';UID='+username+';PWD='+ password)
cursor = cnxn.cursor()

query = "SELECT name, price, quantity FROM [dbo].[sample]"
df = pd.read_sql(query, cnxn)
print(df)

プログラムを実行させるとSQLの結果をDataFrameオブジェクトとして表示させることができる。

$ python sample.py
         name  price  quantity
0       apple    100         3
1  strawberry    110         5
2      banana     90         2
3       grape    150         6
4      orange    200         3
5       peach    130         2
6       lemon    180         4

pandasのDataFrameからSQL Serverのテーブルにデータを挿入

今度は逆にDataFrameに入っているデータをSQL Serverに挿入していく。

サンプルとなるcsvファイルを用意する(department.csv

DepartmentID,Name,GroupName,
1,Engineering,Research and Development,
2,Tool Design,Research and Development,
3,Sales,Sales and Marketing,
4,Marketing,Sales and Marketing,
5,Purchasing,Inventory Management,
6,Research and Development,Research and Development,
7,Production,Manufacturing,
8,Production Control,Manufacturing,
9,Human Resources,Executive General and Administration,
10,Finance,Executive General and Administration,
11,Information Services,Executive General and Administration,
12,Document Control,Quality Assurance,
13,Quality Assurance,Quality Assurance,
14,Facilities and Maintenance,Executive General and Administration,
15,Shipping and Receiving,Inventory Management,
16,Executive,Executive General and Administration

データ挿入先のテーブルを作成

CREATE TABLE [dbo].[department](
    [DepartmentID] INT NOT NULL,
    [Name] VARCHAR(20) NOT NULL,
    [GroupName] VARCHAR(50) NOT NULL
)

pd.read_csv()メソッドを使ってcsvファイルを読み込んでDataFrameへ挿入 iterrows()メソッドでDataFrameオブジェクトから一行ずつ取り出して、データベースへINSERTする

DataFrameオブジェクトのイテレーション周りの処理はこちらが参考になった。

note.nkmk.me

import pyodbc
import pandas as pd

server = '<ホスト名>'
database = '<データベース名>'
username = '<ユーザー名>'
password = '<パスワード>'
cnxn = pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};SERVER='+server +
                      ';DATABASE='+database+';UID='+username+';PWD=' + password)
cursor = cnxn.cursor()

df = pd.read_csv('department.csv')
for index, row in df.iterrows():
    cursor.execute("INSERT INTO [dbo].[department] (DepartmentID,Name,GroupName) values(?,?,?)",
                   row.DepartmentID, row.Name, row.GroupName)
cnxn.commit()
cursor.close()

csvファイルの中身がデータベースに格納されているのがわかる。 f:id:rnakamine:20200920221453p:plain

まとめ

pandasを使うことで簡単にcsvデータをデータベースに挿入できたり、取得できるのでpythonを使ったデータ分析をするのであればぜひ押さえておきたい。 numpyと組み合わせることで、さらに威力を増すようなので、次の機会に試して深掘りしていく。

参考

www.python.ambitious-engineer.com

docs.microsoft.com

マルチステージビルドを使ってDockerにcomposerをインストールする

PHPのパッケージ管理システムであるcomposerをDockerにインストールする際にマルチステージビルドを使うといい感じだったのでメモ。

まずは普通にインストール

composerの公式ドキュメントに従い、PHPを使ってインストールしていく getcomposer.org

  1. composerのインストーラー(PHPファイル)をダウンロード

  2. ダウンロードしたインストーラーのハッシュ値とオリジナルのファイルを比較してファイルの破損・改ざんが無いかチェック

  3. インストーラーを実行し、composerをインストール

  4. インストーラーを削除

Dockerfileに記述するとこんな感じ

FROM php:7.4-cli

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '795f976fe0ebd8b75f26a6dd68f78fd3453ce79f32ecb33e7fd087d39bfeb978342fb73ac986cd4f54edd0dc902601dc') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

しかしこれだと、composerのバージョンが変わるとハッシュ値が変更されてしまい、インストールが失敗してしまう。 となると、毎回composerのバージョンが上がる度にハッシュ値を書き換える必要がある。

マルチステージビルドを使ったインストール

composerの公式のDockerイメージがあるので、マルチステージビルドを使用して以下のように書くことができる、

hub.docker.com

FROM php:7.4-cli

COPY --from=composer /usr/bin/composer /usr/bin/composer

COPY --from=composer /usr/bin/composer /usr/bin/composerの部分で、composerのイメージの中から/usr/bin/composerだけをPHPコンテナの中にコピーでき、毎回バージョンが上がる度にハッシュ値を書き換えなくても良くなる。

そもそもマルチステージビルドとは?

Docker 17.05からの機能で、docker buildを複数のビルドにして実行することができる。

matsuand.github.io

これによって、ビルドするだけの一時的なイメージを作るステージ、デプロイ用のイメージ作るステージを分離することができる。

これで何が嬉しいかというと、例えばビルド済みのイメージから必要なファイルだけをデプロイ用のイメージに取り込むことによってデプロイ用のイメージのサイズを削減することが出来たりする。

goのプログラムをビルドして試してみる

hello worldを表示させるためのプログラムを以下のように記述

hello.go

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

作成したプログラムをビルドするステージ、プログラムを実行するステージに分けて以下のようにDockerfileを記述する。

# ビルドするだけの一時的なイメージを作るステージ
FROM golang:alpine AS build-stage
COPY . /work
WORKDIR /work
RUN go build hello.go

# ビルド済みのバイナリファイルを実行するステージ
FROM busybox
COPY --from=build-stage /work/hello /usr/local/bin/hello
ENTRYPOINT ["/usr/local/bin/hello"]

COPY --from=build-stageとすることで、直前のステージで作り出された生成内容を、単純に新たなステージにコピーしている。 こうすることで、プログラム実行するイメージには、直前のステージでビルドしたバイナリファイルだけをコピーすることができ、無駄のないイメージを作成できることができる。

参考

qiita.com

qiita.com