🐍

(初/中級者向け) Python開発ですぐに役立ちそうなTIPS10選

2023/05/05に公開

roadmap.shのPython Developerを眺めながら、
復習がてら色々調べていたのですが、せっかくなので使えそうなモノを10個ピックアップしました!

※ 分野や紹介するTIPSの粒度が整理されてないです。筆者の思いつきで書いてますので注意を
※ 下のスクラップから抜粋してます
https://zenn.dev/fitness_densuke/scraps/bb0d7ebf99092c

1.pytest実行時のimportエラーの回避策

1.1と1.2セットでやることで効果を発揮します!

1.1 ディレクトリ構成のトップレベルで"src tests"を使い分ける

ツリー階層で表すと以下のような形です。

project
    src
        __init__.py
        module.py
    tests
        test_module.py

ポイント

  • srcディレクトリをtestsと同階層に設けることで相対importせずにfrom src import ・・・ができるようになります。
  • src直下に__init__.pyを置くことでpackageとして認識させるようにしています。
  • testsディレクトリに__init__.pyを置かないようにしています。
    • テストコードは通常、単体で実行するためにディレクトリで構成されており、パッケージとして扱われる必要はないと考えます
    • テストディレクトリ内に別のモジュールを定義した場合、テストが実行される前にそのモジュールが読み込まれ、テストの結果が誤ってしまう可能性があります

1.2 pytestではなくpython -m pytestで呼び出す

python -m pytestについて

ポイント

  • python -m pytestで呼び出すと、カレントディレクトリはをsys.pathに追加してくれます
  • 上記の例ではsrc/をカレントディレクトリの配下に置くことでimportを行うことができます

これについてはpytestのGood Integration Practicesにて言及されています。

If you don’t have a setup.py file and are relying on the fact that Python by default puts the current directory in sys.> path to import your package, you can execute python -m pytest to execute the tests against the local copy directly, > without using pip.
setup.py ファイルがなく、Python がデフォルトでカレントディレクトリを sys.path に置いてパッケージをインポートすることに依存している場合、> python -m pytest を実行すると pip を使わずに直接ローカルコピーに対してテストを実行することができます。

他にもInvoking pytest versus python -m pytestをみると理解が深まります。

そもそもsys.pathとは
sys.pathは、Pythonがモジュールを検索するために使用するディレクトリのリストを保持する変数です。
Pythonがモジュールをインポートするとき、指定されたモジュール名を検索し、見つけた場合はその場所からモジュールを読み込みます。

余談ですが、python3.8が標準ライブラリ, python3.8/site-packagesがサードパーティライブラリのパスです。こうしたパスがあるためこれらのライブラリが使えます。

実践してみたい方向け

こちらにお試し用の環境を作成しました

https://github.com/tamtam-fitness/pytest-sample

参考

https://docs.pytest.org/en/latest/explanation/goodpractices.html

https://qiita.com/ShortArrow/items/32fa54b2c32e09355fed

https://zenn.dev/panyoriokome/articles/f34ae72cc33150

https://stackoverflow.com/questions/4761041/python-import-src-modules-when-running-tests

2. breakpoint

ソースコードの任意の行にbreakpoint()と書きプログラム実行すると、その行で一旦処理をストップして対話モードに入ります。

これに関しては以下の記事がまとまっているので参照すると良いです
https://zenn.dev/gomecha/books/4fa32ac5f76af31b2f81/viewer/9837a3

3.all/ any

all(iterable)...全てのiterableなオブジェクトの中身がTrueかどうか
any(iterable)...一つでもiterableなオブジェクトの中身がTrueかどうか
not any(iterable)... 全てのiterableなオブジェクトの中身がFalseかどうか

こちらはリスト内包表記を応用して任意の条件をallやanyすることができます

li = [0, 1, 2, 3, 4]

print(all([i > 2 for i in li]))
# False

print(any([i > 2 for i in li]))
# True

もちろん、iterableであればいいのでジェネレータで同様なことができます。
(ジェネレータはリストと異なり逐次処理されるので、処理時間やメモリ使用量を抑えられるというメリットがあります)

print(type(i > 2 for i in l))
# <class 'generator'>

print(all(i > 2 for i in l))
# False

print(any(i > 2 for i in l))
# True

4.itertools.group_by

itertools.groupby(iterable, key=None)はiterableの連続した要素をグループ化します。
iterableは、事前にkey関数でソートされている必要があります。


import itertools
from datetime import datetime

data = [
    {"name": "Alice", "birthday": "2000/11/22"},
    {"name": "Bob", "birthday": "1999/2/15"},
    {"name": "Carol", "birthday": "1999/1/25"},
    {"name": "Dave", "birthday": "2000/4/21"}
]

# ソートを事前に行う
data.sort(key=lambda x: x['birthday'])

print(data)

for key, group in itertools.groupby(data, key=lambda x: datetime.strptime(x['birthday'], "%Y/%m/%d").year):
    print(key, list(group))

5.itertools.starmap

聞き馴染みのあるmap関数は、iterable(リスト、タプルなど)に適用するために使用される関数です。

itertools.starmapは各要素がタプルであるiterableを対象とし、関数にタプルの要素を展開して引数として渡します。

import itertools

def multiply(x, y):
    return x * y

pairs = [(1, 2), (3, 4), (5, 6)]
multiplied_numbers = itertools.starmap(multiply, pairs)

# 結果をリストに変換して表示
print(list(multiplied_numbers))  # 出力: [2, 12, 30]

別でmultiprocessのstarmapとかもあるとのこと。

https://qiita.com/okiyuki99/items/a54797cb44eb4ae571f6

6.lambdaのあれこれ

リスト内包表記の利用

li = [lambda arg=x: arg * 10 for x in range(1, 5)]

if-elseの利用

max_func = lambda a, b : a if(a > b) else b
 
print(max_func(1, 2))

複数行でlambdaの利用(x, fという形でlambdaに持たせる)

li = [[2,6,4],[1, 4, 8, 64],[3, 34, 9, 17]]
 
# Sort each sublist
sort_list = lambda x: (sorted(i) for i in x)
 
# Get the second largest element
fetch_second_largest = lambda x, f : [y[len(y)-2] for y in f(x)]
result = fetch_second_largest(li, sort_list)
print(result)

7.functools.reduce

reduce(function, iterable, initializer)は要素同士全てに対してを加算したり減算したりできます。

from functools import reduce
from operator import add
from operator import sub
from operator import mul
from operator import and_
from operator import or_


array = [100, 10, 20, 30, 40, 50]

#add,sub,sub
print(reduce(add, array)) 
print(reduce(sub, array))
print(reduce(mul, array)) 
# 以下と同等
print(reduce(lambda a, b: a+b, array))
print(reduce(lambda a, b: a-b, array)) 
print(reduce(lambda a, b: a*b, array)) 



# or_
print(reduce(or_, ({1}, {1, 2}, {1, 2, 3}, {1, 2, 3, 4})))  # {1, 2, 3, 4}
# nd_
print(reduce(and_, ({1}, {1, 2}, {1, 2, 3}, {1, 2, 3, 4}))) # {1}

Python Tips: functools.reduce() を活用したいの以下の内容がしっくりきました。

これは受け売りですが、 reduce() については、要は「 sum() ・ all() ・ any() 等の「シーケンスからひとつの値を生成する」タイプの処理を抽象> 化したものが reduce() 」という捉え方ができます。もし OOP のクラスと同じような親子関係(継承関係)が関数にもあるなら、 reduce() は sum() > や all() の親関数、と捉えてもよさそうです。

以下ではreduceのパフォーマンスについて言及されています
https://dev.classmethod.jp/articles/python-reduce-vs-generator/

併せて読みたい記事
https://qiita.com/kwatch/items/03fd035a955235681577

reduceの前に内包表記などを検討するなどがいいかもですね

reduce() は、Python3 からは from > functools import reduce をしないと使えなくなりました。これは、reduce() より内包表記やループを使えという意図でしょう。

8.Sorting HOW TO

sorted(iterable, /, *, key=None, reverse=False)などを用いてソートしたい時にこちらを毎回読むと良いと思います。

https://docs.python.org/3/howto/sorting.html#sortinghowto

例えば以下のようなコードが掲載されています

#----------------------
from operator import itemgetter, attrgetter

student_objects = [
    Student('john', 'A', 15),
    Student('jane', 'B', 12),
    Student('dave', 'B', 10),
]
print(sorted(student_objects, key=attrgetter('age')))
# [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]


#----------------------
from operator import itemgetter, attrgetter

student_objects = [
    Student('john', 'A', 15),
    Student('jane', 'B', 12),
    Student('dave', 'B', 10),
]
print(sorted(student_objects, key=attrgetter('age')))
# [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]


#----------------------
Student.__lt__ = lambda self, other: self.age < other.age
print(sorted(student_objects))
# [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

9.zipのあれこれ

みんな大好きzip(*iterables, strict=False)です。

zipの挙動としては短い方のイテラブルが尽きた場合は処理をやめます。

>>> list(zip(range(3), ['fee', 'fi', 'fo', 'fum']))
[(0, 'fee'), (1, 'fi'), (2, 'fo')]

strict=True とすることで、同じ長さのイテラブルであることを保証できます


#-------------------
>>> list(zip(('a', 'b', 'c'), (1, 2, 3), strict=True))
[('a', 1), ('b', 2), ('c', 3)]

#-------------------

>>> for item in zip(range(3), ['fee', 'fi', 'fo', 'fum'], strict=True):  
    print(item)

(0, 'fee')
(1, 'fi')
(2, 'fo')
Traceback (most recent call last):
  ...
ValueError: zip() argument 2 is longer than argument 1]

itertools.zip_longest

itertools.zip_longest(*args, fillvalue=None)

https://docs.python.org/3/library/itertools.html#itertools.zip_longest

こちらを使うことで、短い方のイテラブルが尽きた時に値を埋めることができます。

from  itertools import zip_longest

# zip_longestを使ったサンプルコード
for i in zip_longest('ABCD', 'xy', fillvalue='-'):
    print(i)

# ('A', 'x')
# ('B', 'y')
# ('C', '-')
# ('D', '-')

10. collections.Counter

https://docs.python.org/ja/3/library/collections.html?highlight=collections counter#collections.Counter

リストや文字列の中で要素がどれだけ出現するかを計算する場合、Counterを使用すると簡単かつ効率的に集計ができます。

from collections import Counter

myword = "helloworld!"
mycounter = Counter(myword)
print(mycounter)

# Counter({'l': 3, 'o': 2, '!': 1, 'e': 1, 'd': 1, 'h': 1, 'r': 1, 'w': 1})

most_common([n])

最も多い n 要素を、カウントが多いものから少ないものまで順に並べたリストを返します。
n が省略されるか None であれば、 most_common() はカウンタの すべての 要素を返します。
等しいカウントの要素は挿入順に並べられます:

Counter('abracadabra').most_common(3)
[('a', 5), ('b', 2), ('r', 2)]

Discussion