仕事でpythonを触る機会が今後増えそうなので、pythonに関するメモをためていく予定です。

Pythonのバージョン管理(pyenv)

pyenvを使ってpythonをインストールします

# pyenvのインストール
brew install pyenv
pyenv -v
> pyenv 1.2.13

# インストール可能なpythonのバージョン確認
pyenv install --list

# 特定のバージョンのpythonをインストール
pyenv install  3.7.4

# インストール済みのpythonのバージョン確認
pyenv versions
> * system (set by /Users/rinoguchi/.pyenv/version)
>   3.7.4

# ~/.bash_profileに以下の設定を追加(symsとautocompletionの有効化)
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bash_profile
source ~/.bash_profile

# pythonコマンドの参照先
# - pythonコマンドもpipコマンドも.pyenv/shims配下を見るようになっており
# - 実体はbashで、最終的には指定されたバージョンの.pyenv/versions配下を見にいってくれるぽい
which python
> /Users/rinoguchi/.pyenv/shims/python
which pip
> /Users/rinoguchi/.pyenv/shims/pip

# グローバルで特定のバージョンを使用するよう指定
pyenv global 3.7.4 # .pyenv/versionにバージョン番号が記載される
pyenv versions
>   system (set by /Users/rinoguchi/.pyenv/version)
> * 3.7.4

# 特定のフォルダ配下だけ特定のバージョンを使用するよう指定
# - 実行したフォルダ直下に.pyenv-versionファイル作成される
# - このファイルがこのフォルダ配下ではバージョン固定される
# - gitリポジトリにcommitするのもあり
cd hoge/fuga
pyenv local 2.7.3

# pyenv localを解除
pyenv local --unset
# .pyenv-versionファイル削除される

Pythonのパッケージ管理および仮想環境管理(poetry)

これまでpip + virtualenv(venv)を使っていましたが、poetryに変更しました。詳細は別記事にしたのでそちらを参照ください。
poetryでパッケージ・仮想環境を管理

Pythonのパッケージ管理(pip)

pythonのパッケージ管理システムpipを利用する。これはpython 3.4以降はpython本体と一緒にinstallされます。
パッケージのインストール先は、デフォルトだとpython本体配下の/lib/pythonX.Y/site-packagesフォルダなので、同一のバージョンを使う場合にはインストールされたパッケージは共有されます。

# パッケージをインストールする
pip install numpy

# パッケージを削除する
pip uninstall numpy

# インストール済みパッケージを確認する
pip list

# インストール対象パッケージを設定ファイル(requirements.txt)で管理する
vi requirements.txt

# 現在インストール済みのパッケージを設定ファイル形式で表示する(これをrequirements.txtにコピーする)
pip freeze
> pylint==2.3.1
> pyspark==2.4.3
> numpy==1.17.0

# 設定ファイルにしたがってパッケージをインストールする
pip install -r requirements.txt

# 特定のディレクトリにパッケージをインストールする
# - -tオプションでディレクトリを指定することで可能だが、importする際にはモジュール検索パスを解決が必要になるのでおすすめしない。
pip install -t my_packages numpy

Pythonの仮想環境構築(virtualenv)

virtualenvで隔離されたPythonの仮想環境を構築します。
具体的には、pythonインストール全体が指定したフォルダにコピーされ、コピーされたファイルを利用してpythonコマンドやpipコマンドを実行する形になります。
そのため、プロジェクト固有のpythonバージョン管理、パッケージ管理が可能になります。

# プロジェクトフォルダに移動する
cd {project_folder}

# pyenvでpythonおよびpipのバージョンを指定する
pyenv local 3.7.4

# インストール
pip install --upgrade virtualenv

# 仮想環境構築
# - envは別の名前でもOK。envフォルダにpythonインストール全体のコピーが作成される
virtualenv env

# 仮想環境有効化
# - PATHの一番最初に、`env/bin`を追加している
source env/bin/activate

# pythonやpipコマンドがenv配下のpythonを参照してくれるようになる
which python
> {project_folder}/env/bin/python

# パッケージのインストール
# - このパッケージは他のプロジェクトには影響を与えない
pip install -r requirements.txt

# 仮想環境無効化
# - PATHから`env/bin`を削除している
deactivate

ローカル開発環境での環境変数の設定

ローカル開発環境での環境変数管理はいろんなやり方があります。こちらは別記事にしました。
pythonにおけるローカル環境での環境変数の設定

型ヒントについて

型ヒントを使うことで、静的型付言語のような感覚で実装することができます。こちらは別記事にしました。
pythonの型ヒント

共通モジュールのimport

親兄弟ディレクトリにある共通モジュールをimportする場合、特別な対応をする必要があります。こちらは別記事にしました。
pythonにおける親/兄弟階層のモジュールimportの方法

変数スコープ

こちらの記事で勉強させていただいたのですが、pythonのスコープは4段階あるようです。forifにブロックスコープが無いことに注意が必要です。

  • ビルトインスコープ
    • print int str len range のような組み込み関数のスコープで、一番外側のスコープ。どこからでもアクセスできる
    • ifforのような構文キーワードは予約語なので、これには当たらない
  • グローバルスコープ
    • pythonファイル内で有効なスコープ。モジュールスコープともいうみたい。別ファイル、別モジュールからはアクセスできない
  • エンクロージングスコープ
    • 関数の内側に関数を定義した際に、内側の関数スコープから見たときに、外側の関数スコープをエンクロージングスコープというらしい。クロージャで利用される。
  • ローカルスコープ
    • 関数スコープ。その関数の外側からはアクセスできない

クロージャ

pythonの関数は第一級オブジェクトとして扱うことができるので、クロージャを定義できます。クロージャとは、wikipediaによると

引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。

とのことです。自分の理解では、自身が定義された外側のローカルスコープの変数を使用する関数というイメージです。
クロージャの例でよくあるcounterを実装してみます。

from typing import Callable

def counter():
    cnt: int = 0
    def __count():
        nonlocal cnt  # この宣言によってエンクロージングスコープの変数にアクセスできる
        cnt += 1
        return cnt
    return __count  # ここで__count関数を返却している

fnc: Callable = counter() #  ここで関数オブジェクトを受け取っている
print(fnc())  # この関数はその外側のローカルスコープに定義されている変数(cnt)を参照している
# -> 1
print(fnc())
# -> 2

VSコード設定

型ヒントを利用した静的コードチェック

VSコードはデフォルトでは、型ヒントを指定した静的コードチェックは行ってくれないので、mypyなどのツールを導入する必要があります。
具体的にはsettings.jsonに以下のように設定します。この設定をいれておくと、インストールダイアログが出てくるのでそこからインストールします。

{
    "python.linting.lintOnSave": true,
    "python.linting.mypyEnabled": true,
}

linterおよびformatterの設定

  • linterはflake8、formatterはautopep8を採用
  • どちらもpythonのコード規約であるpep8に準拠しているので相性は良いはず
  • max-line-lengthの設定は両方で合わせる必要がある

具体的にはsettings.jsonに以下のように設定しました。

{
    "python.linting.lintOnSave": true,
    "python.linting.pylintEnabled": false,
    "python.linting.flake8Enabled": true,
    "python.linting.flake8Args": [
        "--ignore=W293, W504",
        "--max-line-length=150",
        "--max-complexity=20"
    ],
    "editor.formatOnSave": true,
    "python.formatting.provider": "autopep8",
    "python.formatting.autopep8Args": [
        "--aggressive",
        "--aggressive",
        "--max-line-length=150",
    ],
}

テストについて

標準モジュールのunittestを使ってシンプルにテストを実装することができます。こちらの記事で使い方をまとめました。
標準モジュールunittestでpythonのテストを書く

実装Tips

タプル ⇄ Array

# タプル -> Array
tuple = (hoge, fuga) = ('xxx', 'yyy')
list(tuple)
# > ['xxx', 'yyy']

# Array -> タプル
array = ['xxx', 'yyy']
tuple(array)
# > ('xxx', 'yyy')

1行でif文を記載する

hoge = get_hoge()
# 1行
return None if hoge == '' else hoge # elseがないと構文エラーになる

# これと同じ意味
if hoge == '':
    return None
else:
    return hoge

リストをN件毎に分割

more-itertools を使うのが良さそう

from more_itertools import chunked
array = [0, 1, 2, 3, 4, 5, 6, 7, 8]
list(chunked(array, 4))
# > [[0, 1, 2, 3], [4, 5, 6, 7], [8]]

部分一致文字列を抽出

import re
string = '山括弧内の文字列を抽出します <hogefuga>'
match = re.search(r'<(.+)>', string)
if match:
    print(match[0])
    print(match[1])
# > <hogefuga>
# > hogefuga

文字列内の文字列の位置を検索

text = '112233445511223344551122334455'
# 開始位置、終了位置を指定しない→一番最初に一致した位置
text.find('11')
# > 0
# 開始位置を指定→開始位置以降で一番最初に一致した位置
text.find('11', 10)
# > 10
# 開始位置と終了位置を指定→範囲内で一番最初に一致した位置
text.find('11', 10, 11)
# > -1
text.find('11', 10, 30)
# > 10

# rfindを使うと、右側から検索する。開始位置や終了位置はfindと同じく左側からカウントする。
text.rfind('11')
# > 20
text.rfind('11', 10)
# > 20
text.rfind('11', 10, 11)
# > -1
text.rfind('11', 10, 12)
# > 10

listとsetとdict

  • listは重複OKで順番ありのリスト。基本はindex指定か、loopして参照する
  • setは重複NGで順番なしのリスト。基本はloopして参照する
  • dictはkeyに対するvalueを保持する辞書。keyは重複を許さない。基本はkeyを指定して参照する
from typing import List, Set, Dict

## list
l = [] #空で初期化
l = [1, 2, 3] #初期化
l: List[int] = [1, 2, 3] #型ヒント付き初期化
l.append(4) #追加
l.remove(4) #value指定削除(同じ要素が複数あったら最初の1つだけ削除)
l.clear() #全削除
l.extend([5, 6]) #listの結合。元のインスタンスが変更される点に注意
l.sort() #ソート。元のインスタンスが変更される点に注意
l.sort(reverse=True) #逆順ソート。元のインスタンスが変更される点に注意
l.sorted() # ソート。元のインスタンスが変更されない
3 in l #存在確認。内部的には要素の`__eq__`メソッドがtrueを返すかどうかで判断

## set
s = set() #空で初期化。`set = {}`だとdictになってしまうのでNG
s = {1, 2, 3, 2, 1} #初期化
s = set({1, 2, 3, 2, 1}) #初期化
# > {1, 2, 3}
s: Set[int] = {1, 2, 3, 2, 1} #型ヒント付き初期化
s: Set[int] = set({1, 2, 3, 2, 1}) #型ヒント付き初期化
# > {1, 2, 3}
s.add(4) #追加
s.remove(4) #削除
s.clear() #全削除
s.update({4, 5, 6}) #一括更新。元のインスタンスが更新される
s |= {4, 5, 6} # updateと同じ
s.union({5, 6}) #setの結合。元のインスタンスが変更されない点に注意
# 要素の存在確認
if 3 in s:
    print('3 exists in s')
# 1件取得
next(iter(s)) # 0件だとエラー
next(iter(s)) if len(s) > 0 else None # 0件を考慮するとこんな感じ
s.pop() # setからitemが削除されることに注意。こちらも0件だとエラー

## dict
d = dict() # 初期化
d = {'key1': 'aaa', 'key2': 'bbb', 'key3': 'ccc'} # 初期化
# > {'key1': 'aaa', 'key2': 'bbb', 'key3': 'ccc'}
d: Dict[str, str] = {'key1': 'aaa', 'key2': 'bbb', 'key3': 'ccc'} # 型ヒント付き初期化
# > {'key1': 'aaa', 'key2': 'bbb', 'key3': 'ccc'}
d['key1'] # 値取得
# > 'aaa'
d.get('key1') # 値取得
# > 'aaa'
d.get('key99', 'xxx') # デフォルト値ありで値取得
# > 'xxx'
d['key4'] = 'ddd' # 追加/更新
# > {'key1': 'aaa', 'key2': 'bbb', 'key3': 'ccc', 'key4': 'ddd'}
d.update({'key4': 'ddd', 'key5': 'eee'}) # 一括更新(結合)
# > {'key1': 'aaa', 'key2': 'bbb', 'key3': 'ccc', 'key4': 'ddd', 'key5': 'eee'}
del(d['key3']) #削除
# > {'key1': 'aaa', 'key2': 'bbb', 'key4': 'ddd', 'key5': 'eee'}
dict(filter(lambda item: item[0] == 'key1', d.items())) # filter -> dict
# > {'key1': 'xxx'}
list(map(lambda item: item[0], d.items())) # map -> list
# > ['key1', 'key2', 'key4', 'key5']
d.clear() #全削除

# set to list
s: Set[int] = set({1, 2, 3, 2, 1})
l: List[int] = list(s)

# list to set
l: List[int] = [1, 2, 3]
s: Set[int] = set(l)
__eq__メソッドと__hash__メソッド
  • __eq__メソッドは、オブジェクト同士の同一性を判定するためのメソッドで==での比較やlistないの存在確認などで使われます。
  • __hash__メソッドは、オブジェクトの同一性を表すhash値を返すメソッドで、__eq__がtrueを返す範囲は同じ値を返すように実装する必要があります。これはdictのkeyなどで使われます。
import dataclasses

@dataclasses.dataclass(frozen=True)
class Test:
  hoge: str
  fuga: str
  def __eq__(self, other):
    return self.hoge == other.hoge # hogeだけで同一性を判定
  def __hash__(self):
    return hash(self.hoge) # hogeだけを元にhashを返す

test1 = Test('a1', 'b1')
test2 = Test('a1', 'b2')
test3 = Test('a1', 'b3')
test4 = Test('a2', 'b3')

test1 == test2 == test3
# > True # hogeが一致しているのでtrue
test3 == test4
# > False # hogeが不一致なのでfalse

l = [test1, test2]
test3 in l
# > True # hoge='a1'の要素がいるのでTrue
l = [test1, test2]
test4 in l
# > False # hoge='a1'の要素がいないのでFalse

d = {test1: 111}
# > {Test(hoge='a1', fuga='b1'): 111}
d[test2] = 222
# > {Test(hoge='a1', fuga='b1'): 222} # test1とtest2は同じhashを返すので、上書きされる
d[test4] = 444
# > {Test(hoge='a1', fuga='b1'): 222, Test(hoge='a2', fuga='b3'): 444} # test4は別のhashなので別で登録される

時間を計測する

import time

started_time = time.time()
# 何らかの処理
finished_time = time.time()
elapsed_time = finished_time - started_time
print("elapsed_time: {} sec".format(elapsed_time))

変数のメモリサイズを確認する

sys.getsizeof()を使います。

import sys

texts = []
for i in range(10000):
  texts.append('{:x=10}'.format(i))
print(sys.getsizeof(texts))
# -> 72

StringIOとBytesIOを使う

公式ドキュメントはこちら

  • StringIO
    
    from io import StringIO
    buffer: StringIO = StringIO('aaa\nbbb\nccc')

readline()で1件ずつ

line: str = buffer.readline()
while line:
print(line)
line = buffer.readline()

readlines()でまとめて

for line in buffer.readlines():
print(line)


* BytesIO
```python
from io import BytesIO, TextIOWrapper
buffer:  BytesIO = getBytesIO() #なんらかの処理でByteIOを取得
wrapper: TextIOWrapper = TextIOWrapper(buffer, encoding='utf-8')
# あとはStringIOと同じやり方でテキストを取得できる

ディレクトリを一気に作成する

import os
dir_path: str = '/tmp/hoge/fuga'
os.makedirs(dir_path, exist_ok=True)
# exist_ok=Trueを指定しないと、すでに存在してたらエラーになる

CSVファイルの出力

csv モジュールを使います。

  • CSVファイルを読み込む
    import csv
    with open('/tmp/hoge/fuga.csv', 'r') as f:
        reader = csv.reader(f)
        for row in reader:
            # row: List[str]
            print(row)
    # > ['aa1', 'bb1', 'cc1']
    # > ['aa2', 'bb2', 'cc2']
    # > ['aa3', 'bb3', 'cc3']
  • CSVファイルに書き出す
    import csv
    with open('/tmp/hoge/fuga.csv', 'w') as f:
        writer = csv.writer(f)
        writer.writerow(['aa1', 'bb1', 'cc1'])
        writer.writerow(['aa2', 'bb2', 'cc2'])
        writer.writerow(['aa3', 'bb3', 'cc3'])

unicodeのままexcelでも開ける形で出力する場合は、こちらを参考に、以下のようにすると良いそうです。

import csv
with open('/tmp/hoge/fuga.tsv', 'w', 'encoding='utf-16'') as f:
    writer = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
    writer.writerow(['ああ1', 'いい1', 'うう1'])
    writer.writerow(['ああ2', 'いい2', 'うう2'])
    writer.writerow(['ああ3', 'いい3', 'うう3'])

ちなみに、encodingを'shift_jis'や'cp932'にする手もあると思いますが、大量データを扱っている場合、UnicodeEncodeErrorが発生する可能性があり、これの対処をするのが大変なので自分はやめました。

for文でloop処理が終わった後に処理を実行させる方法

for文がloopが終わった後に何らかの処理を行いたい場合は、for-else文を使います。ただしelseブロックはbreakした場合には実行されません。
breakせずにloopが全て正常に終わった場合だけ何か処理を行いたい場合に利用するのがいいと思います。

# loop完了後にelseが実行される
for i in [1, 2]:
    print(i)
else:
    print(9)
# > 1
# > 2
# > 9

# 途中でbreakするとelseが実行されない
for i in [1, 2]:
    print(i)
    if i == 2:
        break
else:
    print(9)
# > 1
# > 2

# loop対象が0件の場合もelseが実行される
for i in []:
    print(i)
else:
    print(9)
# > 9

実行中の関数名を取得する

import sys

def hoge():
    print(sys._getframe().f_code.co_name)

hoge()
# > hoge

ファイルの先頭に行を追加する

pythonではファイルの先頭に行を追加することは、すんなりとはできないそうです。
しょうがないので、既存のファイルの内容を取得して先頭に行を追加して、同名のファイルを上書きします。

from typing import List
def add_header_line(file_path: str, header: str):
    with open(file_path) as f:
        lines: List[str] = f.readlines()
    lines.insert(0, header + '\n')
    with open(file_path, mode='w') as f:
        f.writelines(lines)

add_header('/hoge/fuga/text.txt')

辞書(dict)のmapやfilter

dictにはkeyでアクセスするのが鉄則だけど、どうしてもmapやfilterをやりたくなったら以下のようにitems()を使うと実現できる。ただし遅い。

from typing import Dict, Set

# ベース辞書オブジェクト
src_dict: Dict[str, Set[int]] = {'a': {1, 2, 3}, 'b': {4, 5}, 'c': {6, 7, 8}}
# > {'a': {1, 2, 3}, 'b': {4, 5}, 'c': {8, 6, 7}}

# mapの例
mapped_dict: Dict[str, int] = dict((k, len(v)) for k, v in src_dict.items())
# > {'a': 3, 'b': 2, 'c': 3}

# filterの例
filtered_dict_1: Dict[str, Set[int]] = dict((k, v) for k, v in src_dict.items() if len(v) > 2)
filtered_dict_2: Dict[str, Set[int]] = dict(filter(lambda kv: len(kv[1]) > 2, src_dict.items()))
# > {'a': {1, 2, 3}, 'c': {8, 6, 7}}

ジェネレータ関数(GeneratorとAsyncGenerator)

ジェネレータ関数は、値を返す代わりにジェネレータ(一連の値を返す特殊なイテレータ)を返す関数で、yieldキーワードを含む関数はジェネレータ関数です。別記事を書いてます。
pythonのGeneratorとAsyncGeneratorの使い方

subprocessでコマンド実行

単純に実行するだけなら、callrunを使います

from subprocess import run, CompletedProcess

process: CompletedProcess = run(["ls", "-la"], capture_output=True)
print(process.stdout.decode('utf-8'))

処理結果をパイプでつなぐ必要がある場合は、以下のようにPopen(Process Openの略だと思う)とPIPEとcommunicateを使うようです。

from subprocess import Popen, PIPE

p1: Popen = Popen(['ps', 'aux'], stdout=PIPE)
p2: Popen = Popen(['grep', 'python'], stdin=p1.stdout, stdout=PIPE)

stdout, stderr = p2.communicate()
print(stdout.decode('utf-8'))