LoginSignup
752
794

More than 3 years have passed since last update.

Pythonの「痒いところに手が届くなぁ・・・」と思う機能

Last updated at Posted at 2020-02-29

日々のプログラミングに役立つPythonのテクニックや小ネタ、小技をまとめました。

Python 3.8.0で動作確認しています。

3値以上の比較

1 == 2 == 3  # -> False
1 < 2 < 3 < 4  # -> True

時間(datetime/date)の比較

マジックメソッドのおかげで柔軟な比較が出来ます。

参考:[Python] 独自クラスで比較演算ができるようにする

from datetime import date

feb1 = date(2020, 2, 1)
feb2 = date(2020, 2, 2)
feb3 = date(2020, 2, 3)

feb1 < feb2 <= feb2 < feb3  # -> True

時間の最大/最小

from datetime import date

# 意図的に逆順にしてます
dates = [
    date(2020, 2, 3),
    date(2020, 2, 2),
    date(2020, 2, 1),
]

min(dates)  # -> datetime.date(2020, 2, 1)
max(dates)  # -> datetime.date(2020, 2, 3)

時間の計算

# Input
from datetime import datetime

start = datetime(2020, 2, 1, 10)
goal = datetime(2020, 2, 3, 12)

t = goal - start
print(f'あなたの記録は{t.days}日と{t.seconds}秒です')

# Output
'あなたの記録は2日と7200秒です'

時間を連想配列のキーにする

日時ベースの集計をする時に便利です。

# Input
from datetime import date

counts = {
    date(2020, 2, 1): 0,
    date(2020, 3, 1): 0,
}
counts[date(2020, 2, 1)] += 1
counts[date(2020, 2, 1)] += 1
counts[date(2020, 3, 1)] += 1

print(counts)

# Output
{datetime.date(2020, 2, 1): 2, datetime.date(2020, 3, 1): 1}

キーが連想配列に含まれるか

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

print('foo' in d)  # -> True

連想配列からキーを抽出

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d))  # -> ['foo', 'bar', 'baz']

連想配列から値を抽出

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d.values()))  # -> [1, 2, 3]

連想配列からキーと値のペアを抽出

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

for key, value in d.items():
    print(key, value)

# Output
foo 1
bar 2
baz 3

2重配列を連想配列へ変換

# Input
l = [
    ['Yamada', 'baseball'],
    ['Tanaka', 'soccer'],
    ['Sato', 'tennis'],
]
dict(l)

# Output
{'Yamada': 'baseball', 'Tanaka': 'soccer', 'Sato': 'tennis'}

多重配列のループ

# Input
rows = [
    ['yamada', 20],
    ['tanala', 18],
    ['sato', 18],
]

for name, age in rows:
    print(f'{name}さんは{age}歳です')
else:
    print('紹介終わり')

# Output
'yamadaさんは20歳です'
'tanalaさんは18歳です'
'satoさんは18歳です'
'紹介終わり'

多重配列から必要な要素だけ取り出しつつループ

# Input
l = [
    ['Yamada', 'Taro', 20, 'baseball'],
    ['Tanaka', 'Jiro', 18, 'circle'],
]

# 先頭を取り出す
for last_name, *others in l:
    print(last_name, others)
print()

# 末尾を取り出す
for *others, circle in l:
    print(circle, others)
print()

# 最初の2要素を取り出す
# (他の要素が要らない場合はダブルアンダースコアを指定するのがPython流)
for last_name, first_name, *__ in l:
    print(last_name, first_name)

# Output
Yamada ['Taro', 20, 'baseball']
Tanaka ['Jiro', 18, 'circle']

baseball ['Yamada', 'Taro', 20]
circle ['Tanaka', 'Jiro', 18]

Yamada Taro
Tanaka Jiro

このような事もできます。

# Input
l = [
    ['a', 'b', 'c', ['d', 'e', 'f']],
]

for one, *__, (*__, two) in l:
    print(one, two)

# Output
a f

カウンター付きループ

# Input
rows = [
    ['Yamada', 20],
    ['Tanaka', 18],
    ['Sato', 16],
]

for i, (name, age) in enumerate(rows, start=1):
    print(f'{i}行目 : 氏名={name}, 年齢={age}')

# Output
'1行目 : 氏名=Yamada, 年齢=20'
'2行目 : 氏名=Tanaka, 年齢=18'
'3行目 : 氏名=Sato, 年齢=16'

連想配列のキー無しエラーを防ぐ

いちいちif 'キー' in dictと書く必要が無くなります。

d = {
    'Yamada': 20,
    'Tanaka': 18
}
d.get('Yamada')  # -> 20
d.get('Sato')  # -> None
d.get('Sato', '年齢なし')  # -> 年齢なし

配列/連想配列を展開して関数へ渡す

# Input
def func(a, b, c=None, d=None):
    print(a)
    print(b)
    print(c)
    print(d)


l = ['aaa', 'bbb']
d = {'c': 'ccc', 'd': 'ddd'}

func(*l, **d)

# Output
aaa
bbb
ccc
ddd

all関数

配列の全要素がTrueと評価出来るならTrueを返してくれます。

l = [
    True,
    1,
    "foo",
]
all(l)  # -> True

l = [
    True,
    1,
    "",
]
all(l)  # -> False

any関数

配列の要素の内どれか1つでもTrueと評価出来るならTrueを返してくれます。

l = [
    False,
    0,
    "foo",
]
any(l)  # -> True

l = [
    False,
    0,
    "",
]
any(l)  # -> False

any関数の注意点

以下の例では計算コストが高い処理(heavy_task)が無駄に3回も実行されています。

# Input
def heavy_task(ret):
    print(ret)
    return ret


if any([heavy_task(True), heavy_task(False), heavy_task(False)]):
    print('Complete')

# Output
True
False
False
Complete

この場合はifを使ったほうが効率的です。

# Input
def heavy_task(ret):
    print(ret)
    return ret


if heavy_task(True) or heavy_task(False) or heavy_task(False):
    print('Complete')

# Output
True
Complete

集合演算(Set型)

重複要素を削除する

# Input
l = ['yamada', 'tanaka', 'yamada', 'sato']
print(set(l))

# Output
{'sato', 'tanaka', 'yamada'}

重複要素を抜き出す(積集合)

# Input
l1 = ['yamada', 'tanaka', 'yamada', 'sato']
l2 = ['tanaka']
print(set(l1) & set(l2))

# Output
{'tanaka'}

結合した上で重複を削除する(和集合)

# Input
l1 = ['yamada', 'tanaka']
l2 = ['yamada', 'sato']
print(set(l1) | set(l2))

# Output
{'sato', 'tanaka', 'yamada'}

他方から自身に含まれる要素を削除する(差集合)

# Input
l1 = ['yamada', 'tanaka']
l2 = ['tanaka']
print(set(l1) - set(l2))

# Output
{'yamada'}

collections / itertools パッケージ

「このループ処理、愚直に書けば出来るけど面倒くさい・・・」という時に便利なのがcollectionsitertoolsパッケージです。

collections.Counter

要素の出現回数をカウントし、多い順にソートしてくれます。

# Input
import collections

l = ['a', 'b', 'c', 'a', 'a', 'c']
c = collections.Counter(l)
print(c.most_common())

# Output
[('a', 3), ('c', 2), ('b', 1)]

collections.defaultdict

連想配列に対し、キーの有無に関係なく処理を実行出来るようになります。

何を言っているか分からないと思いますのでコード例をご確認ください。

# Input
import json
import collections

# defaultdict()の引数には関数(callable)ならなんでも渡せる
groups = collections.defaultdict(list)

# 普通の連想配列だと "baseballというキーは存在しない" といったエラーが発生する
groups['baseball'].append('yamada')
groups['tennis'].append('tanaka')
groups['baseball'].append('sato')

print(json.dumps(groups))

# Output
{"baseball": ["yamada", "sato"], "tennis": ["tanaka"]}

defaultdictが特に便利だと感じるのは何らかの集計処理を作成する時です。
いちいちキーの存在を確認しなくて良いため処理がシンプルです。

# Input
import collections

products = ['ノート', 'えんぴつ', 'けしごむ']

sales = [
    ['ノート', 1],
    ['けしごむ', 2],
    ['けしごむ', 1],
]

aggregates = collections.defaultdict(int)

for product, cnt in sales:
    aggregates[product] += cnt

print('--- 本日の販売実績 ---')

for product in products:
    print('%s : %d個' % (product, aggregates[product]))

# Output
--- 本日の販売実績 ---
ノート : 1
えんぴつ : 0
けしごむ : 3

関数(callable)ならなんでも渡せるという特性を利用すると、多重連想配列もシンプルに実装できます。

# Input
import json
from collections import defaultdict

nested = defaultdict(lambda: defaultdict(int))

nested['a']['a'] += 1
nested['a']['a'] += 1
nested['a']['b'] += 1
nested['b']['c'] += 1

print(json.dumps(nested))

# Output
{"a": {"a": 2, "b": 1}, "b": {"c": 1}}

itertools.product

2個以上の配列から「組み合わせ」を網羅的に生成してくれます。

# Input
import itertools

a = ['a1', 'a2']
b = ['b1', 'b2', 'b3']
c = ['c1']

list(itertools.product(a, b, c))

# Output
[('a1', 'b1', 'c1'),
 ('a1', 'b2', 'c1'),
 ('a1', 'b3', 'c1'),
 ('a2', 'b1', 'c1'),
 ('a2', 'b2', 'c1'),
 ('a2', 'b3', 'c1')]

itertools.chain.from_iterable

2次元配列を1次元配列に変換してくれます(flatten)。

# Input
import itertools

l = [
    ['a1', 'a2'],
    ['b1', 'b2', 'b3'],
]

list(itertools.chain.from_iterable(l))

# Output
['a1', 'a2', 'b1', 'b2', 'b3']

ioパッケージ

ファイルっぽく扱える仮想オブジェクトを提供してくれます。

テストを書く時に重宝します。

# Input
import io


def writer(f, text):
    f.write(text)


def printer(f):
    print(f.read())


sio = io.StringIO()

writer(sio, 'foo\n')
writer(sio, 'bar\n')
writer(sio, 'baz\n')

sio.seek(0)
printer(sio)

# Output
foo
bar
baz

タプル(tuple)

タプル(tuple)は配列と似ていますが、不変(immutable)であるため値の再代入が行えません。

不変であるが故に、連想配列のキーとして利用できます。

yamada = ('Yamada', 'Taro')  # これがタプル
tanaka = ('Tanaka', 'Jiro')  # これもタプル

print(yamada[0])  # -> Yamada

yamada[0] = 'Tanaka'  # TypeError: 'tuple' object does not support item assignment

ages = {
    yamada: 20,
    tanaka: 18,
}

print(ages[tanaka])  # -> 18

この特性を利用すると、類似データのグルーピングを効率的に行なえます。

# Input
from collections import defaultdict

data = [
    {'circle': 'baseball', 'name': 'yamada', 'age': 10},
    {'circle': 'baseball', 'name': 'sato', 'age': 10},
    {'circle': 'baseball', 'name': 'suzuki', 'age': 11},
    {'circle': 'tennis', 'name': 'tanaka', 'age': 10},
]

per_circle_age = defaultdict(list)

for v in data:
    k = (v['circle'], v['age'])  # (サークル名, 年齢) というタプルを生成
    per_circle_age[k].append(v['name'])  # タプルをキーにして集計

for (circle, age), members in per_circle_age.items():
    print(f'{circle}に所属している{age}歳のメンバー:{members}')

# Output
"baseballに所属している10歳のメンバー:['yamada', 'sato']"
"baseballに所属している11歳のメンバー:['suzuki']"
"tennisに所属している10歳のメンバー:['tanaka']"

dataclassesパッケージ

Python3.7から使える機能です。

コンストラクタにちまちまとself.name = nameの様な代入処理を書かなくて良いので便利です。

import dataclasses


# frozen=True とすると不変(immutable)なオブジェクトとして扱える
@dataclasses.dataclass(frozen=True)
class User:
    last_name: str
    first_name: str

    def full_name(self):
        return f'{self.last_name} {self.first_name}'


yamada = User(last_name='Yamada', first_name='Taro')
tanaka = User(last_name='Tanaka', first_name='Jiro')

yamada.full_name()  # -> Yamada Taro

# 簡単に連想配列へ変換出来ます
dataclasses.asdict(yamada)  # -> {'last_name': 'Yamada', 'first_name': 'Taro'}

# 比較が可能です
yamada2 = User(last_name='Yamada', first_name='Taro')
yamada == yamada2  # -> True
yamada == tanaka  # -> False
yamada in [yamada2]  # -> True

# "frozen=True" とした場合は値の再代入は出来ません
yamada.last_name = 'Sato'  # -> FrozenInstanceError: cannot assign to field 'last_name'

# immutableなので連想配列のキーとして使えます
d = {yamada: 'foo', tanaka: 'bar'}

# 集合演算(Set型)も可能です
{yamada, tanaka} & {yamada2}  # -> {User(last_name='Yamada', first_name='Taro')}
752
794
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
752
794