SIer だけど技術やりたいブログ

Java 経験者がスムーズに Python を使い始めるために

python

これまでは主に Java を読み書きしていたのですが、業務が変わってからはインフラ自動化やデータ分析のためにPython スクリプトを書く機会が増えてきました。最近は世の中的にも、様々な言語やライブラリを適材適所で採用し、組み合わせてシステムを構成することも多いと思います。

ということでこの記事は、Java 経験者がスムーズに Python を使い始めるために知っていると良さそうな事を記載します。

本記事は次の環境で動作確認をしています。

]# cat /etc/redhat-release
CentOS Linux release 8.0.1905 (Core)
]# uname -a
Linux localhost.localdomain 4.18.0-80.11.2.el8_0.x86_64 #1 SMP Tue Sep 24 11:32:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
]# python3 -VV
Python 3.6.8 (default, Oct  7 2019, 17:58:22)
[GCC 8.2.1 20180905 (Red Hat 8.2.1-3)]

はじめに

Python は動的型付け言語。学術系のパッケージが豊富なので機械学習やデータサイエンスの分野で使われることが多い。またインフラの自動化スクリプトを Python で書いたりすることも多い。

名前の由来は『空飛ぶモンティ・パイソン』らしい。

なぜ Python という名前なのですか?
Python の開発が始まった頃、Guido van Rossum は 1970 年代に始まった BBC のコメディシリーズ “Monty Python’s Flying Circus” の台本を読んでいました。Van Rossum は、短くて、ユニークで、少しミステリアスな名前が欲しかったので、この言語の名前を Python と呼ぶことにしたのです。

『空飛ぶモンティ・パイソン』を好きでなくてはいけませんか?
いいえ。でも、好きになってくれるといいな。: )

基本的な Python の説明はPython japan Python とはPython よくある質問に詳細に記載されている。かなり良いことが書いてあるので一読する価値があります。

処理系

Java でいう OpenJDK や OracleJDK のように、様々な実装がある。yum/apt でインストールすると、普通は CPython が入る。

CPython
これは最も保守されている初代のPython実装で、C言語で書かれています。ほとんどの場合、言語の新機能がいち早く実装されます。
Jython
Javaで実装されたPythonです。この実装はJavaアプリケーションのためのスクリプト言語として、もしくはJavaクラスライブラリを使ったアプリケーションを作成するために使用することができます。また、Javaライブラリのテストを作成するためにもしばしば使用されています。さらなる情報については the Jython website を参照してください。
PyPy
完全にPythonで書かれたPythonの実装です。他の実装には見られない、スタックレスのサポートや、実行時 (Just in Time) コンパイラなどの高度な機能をサポートしています。このプロジェクトの一つの目的は、(Pythonで書かれていることによって、)インタプリタを簡単に修正できるようにして、言語自体での実験を後押しすることです。さらなる情報は the PyPy project’s home page にあります。
引用元: Python 言語リファレンス 1.1. 別のPythonの実装

CPython はソースコードを解析して Python バイトコードに 変換してから、それを仮想マシン上で実行する。(Java にそっくり!)

  1. Parse source code into a parse tree (Parser/pgen.c)
  2. Transform parse tree into an Abstract Syntax Tree (Python/ast.c)
  3. Transform AST into a Control Flow Graph (Python/compile.c)
  4. Emit bytecode based on the Control Flow Graph (Python/compile.c)
    引用元: https://www.python.org/dev/peps/pep-0339/

CPython には JIT 機能がない(PyPy などの JIT がある処理系も存在する)ため、実行時に逐一機械語に変換している。また動的型付けのため、最適化がやりにくい。これらの理由から、遅い処理系と言われている。詳細は次が参考になる。
なぜPythonはこんなにも遅いのか?

まあでも、利用用途がコマンドラインのアプリや アドホックな変換/解析処理ならば、そこまで問題にならないとは思う。 Java とは使われ方や重視してるポイントが違うから性能特性も違う。

Python らしさ

『The Zen of Python』として Python が重要視する価値観がまとめられている。

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

プログラマが持つべき心構え (The Zen of Python)が解説付きで分かりやすい。Python 書くなら一度は読んだ方がいいと思います。

対話モード

対話型のインタプリタがある。

]# python3
Python 3.6.8 (default, Oct  7 2019, 17:58:22)
[GCC 8.2.1 20180905 (Red Hat 8.2.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello world")
hello world

またファイルに保存しても実行できる。

#!/usr/bin/env python3

print("hello world")
]# python3 run.py
hello world

自分はライブラリの仕様を確認するために対話型インタプリタを使って、 動くコード片をファイルに貼っていくことが多い。

オフサイドルール

{}ではなく、インデントでコードブロックを表す。
参考 Wikipedia オフサイドルール

>>> if True:
...     print("Always True")
...
Always True

これによって、常にインデントが強制される。 誰が書いたコードでもある程度は同じ見た目になるので、実利的だなと思う。

詳細は Python よくある質問 デザインと歴史 FAQを参照してください。

変数

単に変数名を定義する。動的型付けのため、型宣言はしない。

>>> message = "hello world"
>>> print(message)
hello world

スコープ

for や while にはスコープがない。というかブロックスコープがない。

>>> for i in range(1, 10):
...     pass
...
>>> print(i)
9

スコープは次のようになっていて、最小単位は関数内。下に行くほど広いスコープになる。

  • Local scope(関数単位)
  • Enclosing (function’s) scope,(クロージャで使うやつ)
  • Global Scope(モジュール単位)
  • Built-in scope(組み込み関数など)

同名変数があった場合は狭いスコープが優先される。nonlocalglobalキーワードで、優先するスコープを変更できる。

詳細はPython チュートリアル 9.2. Python のスコープと名前空間Pythonのスコープについてが詳しい。

便利なクラス

文字列

文字列機能がめちゃくちゃリッチ。

フォーマット済み文字列リテラル。

>>> name = "duke"
>>> print(f"I am {name}")
I am duke
>>> print(f"1+1 = {1+1}")
1+1 = 2

文字の幅も簡単に指定できる。

>>> names = ["duke", "tux"]
>>> for name in names:
...     print(f"|{name:5}|")
...
|duke |
|tux  |

複数行の文字列(ドキュメンテーション文字列)もサポートしている。

>>> s="""this is
... multiple line"""
>>> print(s)
this is
multiple line

掛け算もできる。なんじゃこりゃ。

>>> 3 * "bills "
'bills bills bills '

リスト

リストもめちゃくちゃリッチ。

[]でリストを利用できる。なんでも入れられる。(入れていいとは言ってない)

>>> items = [1, "hello", 2.0]
>>> print(items)
[1, 'hello', 2.0]

末尾の,はついていても良い。この仕様のおかげで、値をプログラムで生成したときも楽に扱えるし、複数行で書いてるときに並べ替えもしやすい。

>>> items = [1, 2, 3,]
>>> print(items)
[1, 2, 3]

様々な添え字アクセス[start:end:step]をサポートしている。

>>> items = [1, 2, 3, 4, 5]
# 添え字でアクセスできる
>>> items[0]
1
# マイナスで末尾から指定できる
>>> items[-1]
5
# 範囲指定
>>> items[0:3]
[1, 2, 3]
>>> items[0:5:2]
[1, 3, 5]

リストも足し算や掛け算ができる。

>>> [1,2] + [3,4]
[1, 2, 3, 4]
>>> [1,2] * 2
[1, 2, 1, 2]

リストはミュータブルなクラスなので、 元リストを壊さずに要素を変更したいときは、次のようにしてコピーするテクニックがある。

# 全要素
>>> dup = items[:]
>>> dup[0] = 99
>>> dup
[99, 2, 3, 4, 5]
>>> items
[1, 2, 3, 4, 5]

辞書型

Map 的なやつ。便利。

>>> lang = {'name': 'python', 'age': 28}
>>> lang['name']
'python'

タプル

リストに似ている。 イミュータブルなクラスなので追加・削除はできない。

>>> t = (1, 2, 3) # かっこは無くてもよい
>>> t
(1, 2, 3)
>>> t[1]
2

またアンパック(値の分解)ができるのが強い。

>>> a, b, c = t
>>> a
1
>>> b
2
>>> c
3

要素数が合わない場合はエラーになる。

>>> a, b = t # 要素数が合わないとエラー
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

python では for のイテレータにk, vという形式をとることが多いが、 これにタプルを利用している。

>>> for k, v in {'a':1, 'b':2 }.items():
...     print(k, v)
...
a 1
b 2

またタプルがあることで、関数の戻り値を複数返すことも気軽にできる。 関数の説明は後述する。

>>> def switch(a, b):
...     return (b,a)
...
>>> a, b = switch(1,2)
>>> a
2
>>> b
1

制御構造

比較

比較演算子がすごい。

>>> x=2
>>> if 1 < x < 5:
...     print("True")
...
True

否定はnot

>>> if not(1 == 1):
...     print("never reach")
...

OR は or

>>> if True or False:
...     print("True")
...
True

AND は and

>>> if True and False:
...     print("True")
...

andorは短絡演算子な点に注意。

>>> def plus():
...     global x
...     x+=1
...     return True
...
>>> x=0
>>> if plus() or plus():
...     print(x)
...
1

なお、 Python には++--がないので、インクリメントするときは+=1を使っている。

if/elif

else ifじゃなくてelif

>>> x = int(input("enter an integer: "))
enter an integer: 1
>>> if x < 0:
...     print("negative number")
... elif x > 0:
...     print("positive number")
... else:
...     print("zero")
...
positive number

for

for (int i = 0; i < 10; i++) みたいな形式はない。 for はイテレータごとに回す。

>>> items = ["apple", "banana", "cherry"]
>>> for item in items:
...     print(item)
...
apple
banana
cherry

指定回数繰り返す場合は、rangeと組み合わせる。

>>> for i in range(0, 3):
...     print(i)
...
0
1
2

ここらへんについては以前調べたので、そちらを参考に。

Python for文とイテレータとジェネレータ - SIerだけど技術やりたいブログwww.kimullaa.com

forelseがある。 forを抜けたときにelseが実行される。breakした場合は実行されない。

>>> items = ["apple", "banana", "cherry"]
>>> for item in items:
...     print(item)
... else:
...     print("finished")
...
apple
banana
cherry
finished

while

whileはそこまで変わらない。が、elseがある。 whileを抜けたときにelseが実行される。breakした場合は実行されない。

>>> i = 0
>>> while i < 3:
...     print(i)
...     i += 1
... else:
...     print("else ", i)
...
0
1
2
else  3

switch

switch はない。検討状況は PEP 275で参照できる。

関数

def で関数を定義する。動的型付けのため、引数や戻り値の型宣言は必要ない。

>>> def add(x, y):
...     return x + y
...
>>> add(1, 2)
3

引数は値渡しされるためミュータブルな型を渡すときは注意する。(参照の値渡しなので Java で参照型を引数にするときと一緒)

>>> def append(l):
...     l.append("99")
...
>>> a = [1, 2, 3]
>>> append(a)
>>> a
[1, 2, 3, '99']

また関数はファーストクラスオブジェクト。そのため関数名でアクセスできるし変数に代入もできる。

>>> def add(x, y):
...     return x + y
...
>>> add
<function add at 0x7f8875e54f28>
>>> x = add
>>> x(1, 2)
3

キーワード引数

キーワード引数を使えば、明示的に引数を指定できる。

>>> def show(name, address):
...     print(f"name: {name}, address: {address}")
...
>>> show(address="USA", name="alice")
name: alice, address: USA

引数のデフォルト値

また引数は強制ではなく、引数のデフォルトを設定できる。

>>> def add(x=1, y=1):
...     return x + y
...
>>> add(5)
6

デフォルト引数の注意点

リストのようにミュータブルな型を利用するときは、 関数呼び出しをまたいでデフォルト値が共有されることに注意する。(初めの一回だけ初期化されるため)

>>> def f(a, L=[]):
...     L.append(a)
...     return L
...
>>> print(f(1))
[1]
>>> print(f(2))
[1, 2]
>>> print(f(3))
[1, 2, 3]

*args, **kwargs

*argsで位置引数をタプルで取得できる。 *がついてれば何でもいいが慣習的に*argsが使われる。

>>> def show(*args):
...     print(args)
...
>>> show(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5)

**kwargsで位置引数を辞書型で取得できる。 **がついてれば何でもいいが、慣習的に**kwargsが使われる。

>>> def show(**kwargs):
...     print(kwargs)
...
>>> show(a=1, b=2, c=3)
{'a': 1, 'b': 2, 'c': 3}

*args**kwargsを組み合わせることもできる。 ライブラリは通常、この仕組みを使ってユーザにカスタマイズ性を提供している。

>>> def show(*args, **kwargs):
...     print(args)
...     print(kwargs)
...
>>> show(1, 2, a=3, b=4, c=5)
(1, 2)
{'a': 3, 'b': 4, 'c': 5}

呼び出し元で*args**kwargsを使うと、アンパックされる。

>>> def show(a, b, c):
...     print(a)
...     print(b)
...     print(c)
...
>>> show(*[1,2,3]) # show(1, 2, 3) の呼び出しになる
1
2
3
>>> show(**{'a':1, 'b':2, 'c':3}) # show(a=1, b=2, c=3) の呼び出しになる
1
2
3

*args**kwargsがある場合はこう。

>>> def show(*args, **kwargs):
...     print(args)
...     print(kwargs)
...
>>> kwargs={'a':1, 'b':2}
>>> show(**kwargs) # show(a=1, b=2) の呼び出しになる。が関数で **kwargs に代入される。
()
{'a': 1, 'b': 2}

暗黙的な型宣言

前述の通り、変数や関数で型を明示しない。が、型はある。 むしろ Java のようなプリミティブ型は存在せず、全ていずれかのクラスに属する。

Python における オブジェクト (object) とは、データを抽象的に表したものです。Python プログラムにおけるデータは全て、オブジェクトまたはオブジェクト間の関係として表されます。
引用元: https://docs.python.org/ja/3/reference/datamodel.html#objects-values-and-types

型はtypeで確認できる。

>>> type(1)
<class 'int'>
>>> type(1.0)
<class 'float'>
>>> type("str")
<class 'str'>
>>> type(True)
<class 'bool'>

**動的型付けなので、意図しない型が実行時に渡される可能性がある。**もし型でその演算がサポートされていない場合は、実行時に例外が発生する。

>>> def add(x, y):
...     return x + y
...
>>> add("hello", 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in add
TypeError: must be str, not int

解決するためにはキャストする。(関数内でやってもいいが今回は呼び出し元でキャストする)

>>> add("hello", str(1))
'hello1'

動的型付け が良いか悪いかは何を重視するかによるんでしょうが、 Java やってる方には抵抗感あるんじゃないでしょうか。

まあでも、書いてると慣れます。 (今のところ小さなプログラムしか書いてないので大きなことは言えませんが)

型推論してくれれば文句なしですね。
型推論に関する最近の話題への雑感

クラス

Java とはかなり違う。

クラス宣言

classでクラスを作成できる。

# cat run.py
#!/usr/bin/env python3

class MyClass:
    pass

MyClass()

クラス変数

クラス変数もある。インスタンス間で共有される。

#!/usr/bin/env python3

class MyClass:
    msg = "hello"

print(MyClass.msg)
MyClass.msg = "goodbye"
print(MyClass.msg)
]# ./run.py
hello
goodbye

インスタンス変数

インスタンス変数もある。

  • インスタンスにアクセスする特別なキーワード(this)は存在しない。
  • メソッドの第一引数からインスタンスが参照できる。
  • 第一引数は慣習的にselfとすることが多い。
  • 初期化は__init__で行う。
#!/usr/bin/env python3

class MyClass:
    # コンストラクタ
    def __init__(self, msg):
        self.msg = msg

    def show(self):
        print(self.msg)

# メソッド呼び出し
MyClass("hello").show()
MyClass("goodbye").show()
print("-------")
# プロパティアクセス
print(MyClass("hello").msg)
print(MyClass("goodbye").msg)
]# ./run.py
hello
goodbye
-------
hello
goodbye

インスタンスにアクセスするためにselfを利用するのは少し違和感があるが、インスタンス変数であることを明示できる/親子クラス間でオーバーライドがあったときにも明示できる/インタプリタ上の都合、という様々な理由からこうなっている。

詳細はデザインと歴史 FAQ なぜメソッドの定義や呼び出しにおいて ‘self’ を明示しなければならないのですか?を参照してください。

またインスタンス変数は動的に追加できる。

#!/usr/bin/env python3

class MyClass:
    pass

x = MyClass()
x.msg = "Oops"
print(x.msg)
]# ./run.py
Oops

防ぎたければ__slots__を設定する。

#!/usr/bin/env python3

class MyClass:
    __slots__ = []
    pass

x = MyClass()
x.msg = "Oops"
print(x.msg)
]# ./run.py
Traceback (most recent call last):
  File "./run.py", line 8, in <module>
    x.msg = "Oops"
AttributeError: 'MyClass' object has no attribute 'msg'

メソッドと関数

メソッドと関数は区別されている。 メソッドだけが第一引数にインスタンスが設定されて呼び出される。

#!/usr/bin/env python3

class MyClass:
    msg = "hello"

    def show(self):
        print(self.msg)

print(type(MyClass.show))
print(type(MyClass().show))
MyClass.show()
]# ./run.py
<class 'function'>
<class 'method'>
Traceback (most recent call last):
  File "./run.py", line 9, in <module>
    MyClass.show()
TypeError: show() missing 1 required positional argument: 'self'

メソッドか関数か

Python では多くのグローバル関数が定義されている。
参考 Python 標準ライブラリ 組み込み関数

例えばリストの長さはlen(list)だし、フィルタはfilter(list)

>>> items = [1, 2, 3]
>>> len(items)
3
>>> list(filter(lambda x: x%2, items))
[1, 3]

リストにはこういった操作をするメソッドがない。 また標準ライブラリにはメソッドチェーンをするものも少ない。

例えば Java で次のように書いてた記法は

list.stream()
    .filter(...)
    .filter(...)
    .map(...)
    .collect(Collectors.toList());

Python だとこうなる。

x = filter(... , x)
x = filter(... , x)
x = map(... , x)
list(x)

なぜこうなっているかというと、一目見たときに処理の目的が直感的にわかるかららしい。
参考 Python にメソッドを使う機能 (list.index() 等) と関数を使う機能 (len(list) 等) があるのはなぜですか?

組み込みのモジュールはこの方針が守られているように思うが、ライブラリによってはメソッドチェーンできるように作っているものもあったりして(pandas とか)まあ適材適所なんでしょう。

プライベート変数がない

プライベート変数はない。

_をプライベート相当として扱う慣習がある。

オブジェクトの中からしかアクセス出来ない “プライベート” インスタンス変数は、 Python にはありません。しかし、ほとんどの Python コードが従っている慣習があります。アンダースコアで始まる名前 (例えば _spam) は、 (関数であれメソッドであれデータメンバであれ) 非 public なAPIとして扱います。これらは、予告なく変更されるかもしれない実装の詳細として扱われるべきです。
引用 Python リファレンス 9.6. プライベート変数

名前のマングリング

__(アンダースコア2こ)は名前をマングリングする。
参考 Wikipedia 名前修飾

これは親子クラスでの名前衝突を避けるために 変な修飾名に変換されるだけで、アクセス制御の仕組みではない。

#!/usr/bin/env python3

class MyClass:
    def func(self):
        pass
    def __func(self):
        pass

print(dir(MyClass))
# func は func のまま
# __func が _MyClass_func になる
]# ./run.py
['_MyClass__func', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'func']

継承

継承はclass DerivedClassName(BaseClassName):の形式。

#!/usr/bin/env python3

class MyParentClass:
    def hello(self):
        print("hello from parent")

class MyChildClass(MyParentClass):
    # オーバライドができる
    def hello(self):
        MyParentClass.hello(self)
        print("hello from child")

MyChildClass().hello()
]# ./run.py
hello from parent
hello from child

多重継承

多重継承ができる。Java でも Java8 からのインタフェースのデフォルト実装以降は多重継承っぽいし、驚くことはないと思う。
Java8のインタフェース実装から多重継承とMixinを考える

多重継承は次のように行う。

#!/usr/bin/env python3

class A:
    def hello(self):
        print("hello from A")

class B(A):
    def hello(self):
        print("hello from B")

class C(A):
    def hello(self):
        print("hello from C")

class D(B,C):
    pass

D().hello()

図示するとこのようになる。

実行すると B が呼ばれる。

]# ./run.py
hello from B

メソッド解決順序には C3線形化 というアルゴリズムを使っている。__mro__で解決順序が出るので、これを手掛かりにするとよい。

print(D.__mro__)

(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

メソッド解決順序の詳細はThe Python 2.3 Method Resolution Orderを参照してください。

オーバロードはない

オーバロードはできない。 同名関数が多重に定義された場合、後ろが有効になる。

#!/usr/bin/env python3

class MyClass:
    def func(self, arg1):
        print("func(self, arg1)")
    def func(self, arg1, arg2):
        print("func(self, arg1,arg2)")

MyClass().func(1)
]# ./run.py
Traceback (most recent call last):
  File "./run.py", line 9, in <module>
    MyClass().func(1)
TypeError: func() missing 1 required positional argument: 'arg2'

デコレータ

AOP 的なことができる。利用用途はトランザクション管理やログ管理などの横断的な関心事をロジックから分離するのに使う。

しかもアノテーションを処理する箇所が言語仕様上、明らかになっている

#!/usr/bin/env python3

def log(handler):
    def wrapper(*args, **kwargs):
        print(handler.__name__, "start")
        handler(*args, **kwargs)
        print(handler.__name__, "end")
    return wrapper

@log
def func():
    print("do something")

func()
]# ./run.py
func start
do something
func end

Spring Framework を使ってるときに良くあった「アノテーション処理してる所どこだよ!」という苦痛を味わわなくて済みます。これ本当に、最高じゃないでしょうか!

詳細はPythonのデコレータについてを参照してください。

setter/getter

python にはプライベート変数がない。そのため、setterがあるのに別の経路から値を書き換えることができる。

#!/usr/bin/env python3

class MyClass:
    def __init__(self):
        self._v = 0

    def get_v(self):
        return self._v

    def set_v(self, v):
        self._v = v ** 2

x = MyClass()
x.v = 2 # setter 経由なら 2 倍した値が設定されるはずなのに…
print(x.v)
]# ./run.py
2

それを防ぐことができる仕組み。 @property@v.setterを付けるとgetter/setterの利用を強制できる。

#!/usr/bin/env python3

class MyClass:
    def __init__(self):
        self._v = 0

    @property
    def v(self):
        return self._v

    @v.setter
    def v(self, v):
        self._v = v ** 2

x = MyClass()
x.v = 2 # 勝手に setter メソッド経由になる
print(x.v)
]# ./run.py
4

クラスの拡張

自分で作成したクラスに対して演算子(項算術演算子 (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |))を定義できる。(Scala っぽさありますね)

#!/usr/bin/env python3

class MyClass:
    def __init__(self, v):
        self._v = v

    # 足し算
    def __add__(self, other):
        return MyClass(self._v + other._v)

# 足し算ができる(MyClass(3) と同値)
MyClass(1) + MyClass(2)

またビルトイン関数に対する関数も定義できる。 標準ライブラリが外部ライブラリに対しても動作するのは、これが理由。

#!/usr/bin/env python3

class MyClass:
    def __init__(self, v):
        self._v = v

    # str()
    def __str__(self):
        return str(self._v)

    # len()
    def __len__(self):
        return 1

print(str(MyClass(1)))
print(len(MyClass(1)))
]# ./run.py
1
1

クラスの調査

dir でオブジェクトのメソッドや変数を一覧表示できる。

>>> dir("1")
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

help で説明が読める。

>>> help("1".title)

Help on built-in function title:

title(...) method of builtins.str instance
    S.title() -> str

    Return a titlecased version of S, i.e. words start with title case
    characters, all remaining cased characters have lower case.
(END)

例外

例外はそこまで変わらないように思う。

try - except で例外キャッチができる。

>>> while True:
...     try:
...         x=int("a")
...     except ValueError:
...         print("Oops")
...         break
...
Oops

全てキャッチする場合(catch(Throwable)的な)はexcept:にする。

また複数の例外をキャッチするexcept句も書ける。

... except (RuntimeError, TypeError, NameError):

例外スローはraise

>>> try:
...     raise Exception("Oops")
... except Exception as e:
...     print(e)
...
Oops

クリーンアップ処理はfinally

>>> try:
...     raise Exception("Oops")
... except Exception as e:
...     print(e)
... finally:
...     print("cleanup")
...
Oops
cleanup

with

クリーンアップ処理を強制的に行わせる方法。 (AutoCloseableを汎用にした感じ)

# ファイルクローズが自動で行われる
with open("a.txt", "r") as f:
    ...

Python ではこの仕組みを簡単に自分で作れる。
参考 Python リファレンス 8.5. with 文

# cat run.py
#!/usr/bin/env python3

class MyClass:
    # コンテキストに入る時
    def __enter__(self):
        print('START CONTEXT')
        return self

    # コンテキストを出る時
    def __exit__(self, ex_type, ex_value, traceback):
        print('END CONTEXT')

    def do_something(self):
        print('DO SOMETHING')

with MyClass() as c:
    c.do_something()
# python3 run.py
START CONTEXT
DO SOMETHING
END CONTEXT

いい感じですよね。

標準ライブラリが充実

標準ライブラリ充実してます。バッテリー同梱だそうです。

詳しくは Python チュートリアル 10. 標準ライブラリミニツアーPython 標準ライブラリを参照してください。

docstring

関数やクラスにドキュメントを記載できる。

>>> def func():
...     """
...     do something
...     """
...
>>> func.__doc__
'\n    do something\n    '

helpで表示されるのはコレ。

>>> help(func)

Help on function func in module __main__:

func()
    do something
(END)

ドキュメントのルールや記載方法はPython チュートリアル 4.7.7. ドキュメンテーション文字列[Python]可読性を上げるための、docstringの書き方を学ぶ(NumPyスタイル)を参照してください。

これらを書いてると pydoc でドキュメントが生成できる。
参考 pydoc --- ドキュメント生成とオンラインヘルプシステム

モジュール

プログラムを分割して管理できる。

分割したモジュールはimportで読み込む。 モジュール内の関数はモジュール名.関数名でアクセスできる。

]# cat calc.py
def add(x, y):
    return x + y
]# cat run.py
#!/usr/bin/env python3

import calc

print(calc.add(1, 2))

モジュールをインポートするときに、別名をつけられる。

#!/usr/bin/env python3

import calc as a

print(a.add(1, 2))

from ... import ..を利用すると、モジュールを直接参照できる。

#!/usr/bin/env python3

from calc import add

print(add(1, 2))

むやみにモジュールを直接参照する(from calc import *)のは絶対にやめるべき。『The Zen of Python』にも書いている。

Namespaces are one honking great idea — let’s do more of those!

モジュールは-mオプションで実行できる。 importと直接実行されたときを分離するために if __name__ == "__main__":というイディオムが使われる。

def add(x, y):
    return x + y

if __name__ == "__main__":
    print(add(1, 2))
]# python3 -m calc
3

様々な標準モジュールが存在する。 例えば次のようにすれば、カレントディレクトリでhttpサーバを起動できる。

]# python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

外部ライブラリも山ほどある。 これはpipなどでダウンロードできる。

# pip3 install pandas
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Collecting pandas
  Using cached https://files.pythonhosted.org/packages/52/3f/f6a428599e0d4497e1595030965b5ba455fd8ade6e977e3c819973c4b41d/pandas-0.25.3-cp36-cp36m-manylinux1_x86_64.whl
Requirement already satisfied: python-dateutil>=2.6.1 in /usr/lib/python3.6/site-packages (from pandas)
Requirement already satisfied: pytz>=2017.2 in /usr/lib/python3.6/site-packages (from pandas)
Requirement already satisfied: numpy>=1.13.3 in /usr/local/lib64/python3.6/site-packages (from pandas)
Requirement already satisfied: six>=1.5 in /usr/lib/python3.6/site-packages (from python-dateutil>=2.6.1->pandas)
Installing collected packages: pandas
Successfully installed pandas-0.25.3
#!/usr/bin/env python3
import pandas as pd

s = pd.Series([2,4,6,8,10])
print(s)
]# ./run.py
0     2
1     4
2     6
3     8
4    10
dtype: int64

モジュールの検索パスはsys.pathで表示できる。

>>> import sys
>>> sys.path
['', '/usr/lib64/python36.zip', '/usr/lib64/python3.6', '/usr/lib64/python3.6/lib-dynload', '/usr/local/lib64/python3.6/site-packages', '/usr/lib64/python3.6/site-packages', '/usr/lib/python3.6/site-packages']

pip でインストールされたモジュールも上記のパスに格納される。

]# ls /usr/local/lib64/python3.6/site-packages/pandas
__init__.py  _libs        api     conftest.py  io          tests
__pycache__  _typing.py   arrays  core         plotting    tseries
_config      _version.py  compat  errors       testing.py  util

開発環境

実行環境/ライブラリの管理

新しいものがバンバン出る。とりあえず Pipenv を使えば、Pythonバージョンからライブラリバージョンまでをファイルで一元管理できる。Java でいう Maven 相当のことはできます。
参考 Pipenv: 人間のためのPython開発ワークフロー

これまでの経緯はPythonのパッケージ周りのベストプラクティスを理解するが詳しい。

IDE

PyCharmJupyter Notebook あたりが有名。

Python で大規模コードを書くなら IDE はしっかり考えた方がいいとは思うが、 自分は軽いスクリプトを書くくらいなので vim か 対話型インタプリタで済ましてる。pandas とかグラフィカルなものを書くときは Jupyter Notebook を使う。

紹介サイトも色々あるのでそちらを参考に。
参考 PythonのIDE(統合開発環境)でオススメなものは?現役エンジニアが解説【初心者向け】

フォーマット

yapf や black などがある。PEP 8 というフォーマットのガイドラインがあるので、まずはそれに合わせておけば良いと思う 自分は yapf を使っている。
参考 Pythonのスタイルガイドとそれを守るための各種Lint・解析ツール5種まとめ!
参考 これで決まり!最強自動コード整形ツール3選!

デバッグ

printデバッグで済ませるか pdbがある。gdb に慣れてる人は抵抗なく pdb が使えると思う。

テスト

標準のテスティングツールは 2 種類ある。

  • unittest

  • doctest

    • コメントにテストコードを記載する。
    • 対話的インタプリタの結果を書くと、その通りに実行されるかを確認できる。
    • doctest

doctest は Java やってると馴染みない使い方のため紹介する。

#!/usr/bin/env python3
"""
>>> add(1, 3)
4
"""

def add(x, y):
    return x + y + 1

if __name__ == "__main__":
    import doctest
    doctest.testmod()
]# ./run.py
**********************************************************************
File "./run.py", line 4, in __main__
Failed example:
    add(1, 3)
Expected:
    4
Got:
    5
**********************************************************************
1 items had failures:
   1 of   1 in __main__
***Test Failed*** 1 failures.

テストでバグを見つけるというよりは、一回作ったツールをその後も壊さずにリファクタリングするために使うといいんじゃないかと思う。

最後に

アレもコレもと書いてるうちにけっこうな分量になりましたが、少ない言語仕様でも拡張性があるように考えられている言語だと感じます。面白い言語ですし、他の言語を知ってると楽しいのでぜひ Python やりましょう!