Python 3.7の新機能
Python 3.7はofficially releasedです! この新しいPythonバージョンはSeptember 2016から開発中であり、今ではコア開発者の努力の結果を楽しむことができます。
新しいPythonバージョンは何をもたらしますか? documentationは新機能の概要を示していますが、この記事では、いくつかの最大のニュースについて詳しく説明します。 これらが含まれます:
-
新しい
breakpoint()
ビルトインによるデバッガーへのより簡単なアクセス -
データクラスを使用した簡単なクラス作成
-
モジュール属性へのカスタマイズされたアクセス
-
タイプヒンティングのサポートの改善
-
高精度のタイミング関数
さらに重要なことに、Python 3.7は高速です。
この記事の最後のセクションでは、この速度のほか、Python 3.7のその他の優れた機能について詳しく説明します。 また、新しいバージョンへのアップグレードに関するアドバイスも得られます。
breakpoint()
ビルトイン
私たちは完璧なコードを書くよう努力するかもしれませんが、単純な真実は私たちが決してしないということです。 デバッグはプログラミングの重要な部分です。 Python 3.7では、新しい組み込み関数breakpoint()
が導入されています。 これは実際にはPythonに新しい機能を追加するものではありませんが、デバッガーの使用をより柔軟かつ直感的にします。
ファイルbugs.py
に次のバグのあるコードがあると仮定します。
def divide(e, f):
return f / e
a, b = 0, 1
print(divide(a, b))
コードを実行すると、divide()
関数内にZeroDivisionError
が発生します。 コードを中断して、divide()
のすぐ上にあるdebuggerにドロップするとします。 これを行うには、コードにいわゆる「ブレークポイント」を設定します。
def divide(e, f):
# Insert breakpoint here
return f / e
ブレークポイントは、プログラム内の現在の状態を確認できるように、実行が一時的に停止する必要があるコード内のシグナルです。 ブレークポイントをどのように配置しますか? Python 3.6以下では、このやや不可解な行を使用します。
def divide(e, f):
import pdb; pdb.set_trace()
return f / e
ここで、pdb
は標準ライブラリのPythonデバッガです。 Python 3.7では、代わりに新しいbreakpoint()
関数呼び出しをショートカットとして使用できます。
def divide(e, f):
breakpoint()
return f / e
バックグラウンドでは、breakpoint()
は最初にpdb
をインポートし、次にpdb.set_trace()
を呼び出します。 明らかな利点は、breakpoint()
が覚えやすく、27文字ではなく12文字を入力するだけでよいことです。 ただし、breakpoint()
を使用することの本当の利点は、そのカスタマイズ性です。
breakpoint()
を使用してbugs.py
スクリプトを実行します。
$ python3.7 bugs.py
> /home/gahjelle/bugs.py(3)divide()
-> return f / e
(Pdb)
スクリプトはbreakpoint()
に達すると中断し、PDBデバッグセッションに移動します。 c
と入力し、[.kbd .key-enter]#Enter#を押して、スクリプトを続行できます。 PDBとデバッグについて詳しく知りたい場合は、Nathan Jennings’ PDB guideを参照してください。
次に、バグを修正したと思うと言います。 デバッガーで停止せずに、スクリプトを再度実行する必要があります。 もちろん、breakpoint()
行をコメントアウトすることもできますが、別のオプションはPYTHONBREAKPOINT
環境変数を使用することです。 この変数はbreakpoint()
の動作を制御し、PYTHONBREAKPOINT=0
を設定すると、breakpoint()
への呼び出しはすべて無視されます。
$ PYTHONBREAKPOINT=0 python3.7 bugs.py
ZeroDivisionError: division by zero
バグを修正していないようです。
もう1つのオプションは、PYTHONBREAKPOINT
を使用してPDB以外のデバッガーを指定することです。 たとえば、PuDB(コンソールのビジュアルデバッガー)を使用するには、次の操作を実行できます。
$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py
これを機能させるには、pudb
(pip install pudb
)をインストールする必要があります。 ただし、Pythonがpudb
のインポートを処理します。 この方法で、デフォルトのデバッガを設定することもできます。 PYTHONBREAKPOINT
環境変数を好みのデバッガーに設定するだけです。 システムに環境変数を設定する方法については、this guideを参照してください。
新しいbreakpoint()
関数は、デバッガーでのみ機能するわけではありません。 便利なオプションの1つは、コード内でインタラクティブシェルを単純に起動することです。 たとえば、IPythonセッションを開始するには、次を使用できます。
$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]: print(e / f)
0.0
独自の関数を作成して、breakpoint()
にそれを呼び出させることもできます。 次のコードは、ローカルスコープ内のすべての変数を出力します。 それをbp_utils.py
というファイルに追加します。
from pprint import pprint
import sys
def print_locals():
caller = sys._getframe(1) # Caller is 1 frame up.
pprint(caller.f_locals)
この関数を使用するには、<module>.<function>
表記を使用して、以前と同じようにPYTHONBREAKPOINT
を設定します。
$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py
{'e': 0, 'f': 1}
ZeroDivisionError: division by zero
通常、breakpoint()
は、引数を必要としない関数およびメソッドを呼び出すために使用されます。 ただし、引数を渡すこともできます。 bugs.py
の行breakpoint()
を次のように変更します。
breakpoint(e, f, end="<-END\n")
Note:デフォルトのPDBデバッガーは、pdb.set_trace()
が位置引数をとらないため、この行でTypeError
を発生させます。
print()
関数を装ったbreakpoint()
を使用してこのコードを実行し、渡される引数の簡単な例を確認します。
$ PYTHONBREAKPOINT=print python3.7 bugs.py
0 1<-END
ZeroDivisionError: division by zero
詳細については、PEP 553、およびbreakpoint()
とsys.breakpointhook()
のドキュメントを参照してください。
データクラス
新しいdataclasses
モジュールでは、.__init__()
、.__repr__()
、.__eq__()
などの特別なメソッドが自動的に追加されるため、独自のクラスを作成する方が便利です。 @dataclass
デコレータを使用すると、次のように記述できます。
from dataclasses import dataclass, field
@dataclass(order=True)
class Country:
name: str
population: int
area: float = field(repr=False, compare=False)
coastline: float = 0
def beach_per_person(self):
"""Meters of coastline per person"""
return (self.coastline * 1000) / self.population
これらの9行のコードは、かなりの定型コードとベストプラクティスを表しています。 Country
を通常のクラスとして実装するために必要なことを考えてください:.__init__()
メソッド、repr
、6つの異なる比較メソッド、および.beach_per_person()
メソッド。 下のボックスを展開すると、データクラスとほぼ同等のCountry
の実装を確認できます。
作成後、データクラスは通常のクラスになります。 たとえば、通常の方法でデータクラスから継承できます。 データクラスの主な目的は、堅牢なクラス、特に主にデータを格納する小さなクラスをすばやく簡単に記述できるようにすることです。
Country
データクラスは、他のクラスと同じように使用できます。
>>>
>>> norway = Country("Norway", 5320045, 323802, 58133)
>>> norway
Country(name='Norway', population=5320045, coastline=58133)
>>> norway.area
323802
>>> usa = Country("United States", 326625791, 9833517, 19924)
>>> nepal = Country("Nepal", 29384297, 147181)
>>> nepal
Country(name='Nepal', population=29384297, coastline=0)
>>> usa.beach_per_person()
0.06099946957342386
>>> norway.beach_per_person()
10.927163210085629
クラスの初期化時には、すべてのフィールド.name
、.population
、.area
、および.coastline
が使用されることに注意してください(ただし、.coastline
はオプションですが、内陸国ネパールの例)。 Country
クラスには妥当なrepr
がありますが、メソッドの定義は通常のクラスの場合と同じように機能します。
デフォルトでは、データクラスの同等性を比較できます。 @dataclass
デコレータでorder=True
を指定したので、Country
クラスもソートできます。
>>>
>>> norway == norway
True
>>> nepal == usa
False
>>> sorted((norway, usa, nepal))
[Country(name='Nepal', population=29384297, coastline=0),
Country(name='Norway', population=5320045, coastline=58133),
Country(name='United States', population=326625791, coastline=19924)]
ソートはフィールド値で行われ、最初に.name
、次に.population
というように続きます。 ただし、field()
を使用する場合は、比較で使用されるフィールドをcustomizeできます。 この例では、.area
フィールドはrepr
と比較から除外されています。
Note:国のデータはCIA World Factbookからのもので、人口数は2017年7月と推定されています。
ノルウェーでの次のビーチホリデーを予約する前に、ファクトブックでNorwegian climateについて次のように述べています。「北大西洋海流によって変更された海岸沿いの温帯。降水量が増加し、夏が寒くなる、より寒いインテリア。西海岸では一年中雨が降っています。」
データクラスは、namedtuple
と同じことをいくつか実行します。 それでも、彼らはattrs
projectから最大のインスピレーションを得ています。 その他の例と詳細についてはfull guide to data classesを、公式の説明についてはPEP 557を参照してください。
モジュール属性のカスタマイズ
属性はPythonのどこにでもあります! クラス属性はおそらく最も有名ですが、実際には属性は、関数やモジュールなど、本質的に何にでも配置できます。 Pythonの基本機能のいくつかは、属性として実装されます。ほとんどのイントロスペクション機能、ドキュメント文字列、名前空間です。 モジュール内の関数は、モジュール属性として利用可能になります。
ほとんどの場合、属性はドット表記thing.attribute
を使用して取得されます。 ただし、getattr()
を使用して実行時に名前が付けられた属性を取得することもできます。
import random
random_attr = random.choice(("gammavariate", "lognormvariate", "normalvariate"))
random_func = getattr(random, random_attr)
print(f"A {random_attr} random value: {random_func(1, 1)}")
このコードを実行すると、次のようなものが生成されます。
A gammavariate random value: 2.8017715125270618
クラスの場合、thing.attr
を呼び出すと、最初にthing
で定義されたattr
が検索されます。 見つからない場合は、特別なメソッドthing.__getattr__("attr")
が呼び出されます。 (これは単純化です。 詳細については、this articleを参照してください。).__getattr__()
メソッドを使用して、オブジェクトの属性へのアクセスをカスタマイズできます。
Python 3.7までは、モジュール属性に対して同じカスタマイズを簡単に利用できませんでした。 ただし、PEP 562は、対応する__dir__()
関数とともに、モジュールに__getattr__()
を導入します。 __dir__()
特殊関数を使用すると、dir()
on a moduleを呼び出した結果をカスタマイズできます。
PEP自体は、これらの関数の使用方法のいくつかの例を示しています。これには、関数への非推奨警告の追加や、重いサブモジュールの遅延読み込みが含まれます。 以下では、関数をモジュールに動的に追加できる単純なプラグインシステムを構築します。 この例では、Pythonパッケージを利用しています。 パッケージの復習が必要な場合は、this articleを参照してください。
新しいディレクトリplugins
を作成し、次のコードをファイルplugins/__init__.py
に追加します。
from importlib import import_module
from importlib import resources
PLUGINS = dict()
def register_plugin(func):
"""Decorator to register plug-ins"""
name = func.__name__
PLUGINS[name] = func
return func
def __getattr__(name):
"""Return a named plugin"""
try:
return PLUGINS[name]
except KeyError:
_import_plugins()
if name in PLUGINS:
return PLUGINS[name]
else:
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}"
) from None
def __dir__():
"""List available plug-ins"""
_import_plugins()
return list(PLUGINS.keys())
def _import_plugins():
"""Import all resources to register plug-ins"""
for name in resources.contents(__name__):
if name.endswith(".py"):
import_module(f"{__name__}.{name[:-3]}")
このコードの機能を確認する前に、plugins
ディレクトリ内にさらに2つのファイルを追加します。 まず、plugins/plugin_1.py
を見てみましょう。
from . import register_plugin
@register_plugin
def hello_1():
print("Hello from Plugin 1")
次に、ファイルplugins/plugin_2.py
に同様のコードを追加します。
from . import register_plugin
@register_plugin
def hello_2():
print("Hello from Plugin 2")
@register_plugin
def goodbye():
print("Plugin 2 says goodbye")
これらのプラグインは次のように使用できるようになりました。
>>>
>>> import plugins
>>> plugins.hello_1()
Hello from Plugin 1
>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']
>>> plugins.goodbye()
Plugin 2 says goodbye
これはすべて革命的であるとは思われないかもしれませんが(おそらくそうではありません)、実際にここで何が起こったのか見てみましょう。 通常、plugins.hello_1()
を呼び出せるようにするには、hello_1()
関数をplugins
モジュールで定義するか、plugins
パッケージの__init__.py
内に明示的にインポートする必要があります。 ここでは、どちらでもありません!
代わりに、hello_1()
はplugins
パッケージ内の任意のファイルで定義され、hello_1()
は@register_plugin
%を使用して自身を登録することにより、plugins
パッケージの一部になります。 (t5)s。
違いは微妙です。 使用可能な機能を指示するパッケージの代わりに、個々の機能はパッケージの一部として登録されます。 これにより、使用可能な関数の集中リストを保持することなく、コードの残りの部分から独立して関数を追加できる単純な構造が得られます。
plugins/__init__.py
コード内で__getattr__()
が何をするかを簡単に確認しましょう。 plugins.hello_1()
を要求すると、Pythonは最初にplugins/__init__.py
ファイル内のhello_1()
関数を探します。 そのような関数は存在しないため、Pythonは代わりに__getattr__("hello_1")
を呼び出します。 __getattr__()
関数のソースコードを覚えておいてください。
def __getattr__(name):
"""Return a named plugin"""
try:
return PLUGINS[name] # 1) Try to return plugin
except KeyError:
_import_plugins() # 2) Import all plugins
if name in PLUGINS:
return PLUGINS[name] # 3) Try to return plugin again
else:
raise AttributeError( # 4) Raise error
f"module {__name__!r} has no attribute {name!r}"
) from None
__getattr__()
には、次の手順が含まれています。 次のリストの番号は、コード内の番号付きコメントに対応しています。
-
まず、関数は楽観的に
PLUGINS
ディクショナリから名前付きプラグインを返そうとします。 これは、name
という名前のプラグインが存在し、すでにインポートされている場合に成功します。 -
名前付きプラグインが
PLUGINS
ディクショナリに見つからない場合は、すべてのプラグインがインポートされていることを確認します。 -
インポート後に名前付きプラグインが使用可能になった場合、そのプラグインを返します。
-
すべてのプラグインをインポートした後、プラグインが
PLUGINS
ディクショナリにない場合、name
は現在のモジュールの属性(プラグイン)ではないことを示すAttributeError
を発生させます。
しかし、PLUGINS
ディクショナリはどのように入力されますか? _import_plugins()
関数はplugins
パッケージ内のすべてのPythonファイルをインポートしますが、PLUGINS
には触れていないようです。
def _import_plugins():
"""Import all resources to register plug-ins"""
for name in resources.contents(__name__):
if name.endswith(".py"):
import_module(f"{__name__}.{name[:-3]}")
各プラグイン関数は@register_plugin
デコレータによって装飾されていることを忘れないでください。 このデコレータは、プラグインがインポートされたときに呼び出され、実際にPLUGINS
ディクショナリに入力されます。 プラグインファイルの1つを手動でインポートすると、これを確認できます。
>>>
>>> import plugins
>>> plugins.PLUGINS
{}
>>> import plugins.plugin_1
>>> plugins.PLUGINS
{'hello_1': }
例を続けると、モジュールでdir()
を呼び出すと、残りのプラグインもインポートされることに注意してください。
>>>
>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']
>>> plugins.PLUGINS
{'hello_1': ,
'hello_2': ,
'goodbye': }
dir()
は通常、オブジェクトで使用可能なすべての属性を一覧表示します。 通常、モジュールでdir()
を使用すると、次のようになります。
>>>
>>> import plugins
>>> dir(plugins)
['PLUGINS', '__builtins__', '__cached__', '__doc__',
'__file__', '__getattr__', '__loader__', '__name__',
'__package__', '__path__', '__spec__', '_import_plugins',
'import_module', 'register_plugin', 'resources']
これは有用な情報かもしれませんが、利用可能なプラグインを公開することに関心があります。 Python 3.7では、__dir__()
特殊関数を追加することにより、モジュールでdir()
を呼び出した結果をカスタマイズできます。 plugins/__init__.py
の場合、この関数は最初にすべてのプラグインがインポートされていることを確認してから、それらの名前を一覧表示します。
def __dir__():
"""List available plug-ins"""
_import_plugins()
return list(PLUGINS.keys())
この例を終了する前に、Python 3.7の別のクールな新機能も使用したことに注意してください。 plugins
ディレクトリ内のすべてのモジュールをインポートするために、新しいimportlib.resources
モジュールを使用しました。 このモジュールは、__file__
ハック(常に機能するとは限りません)またはpkg_resources
(遅い)を必要とせずに、モジュールおよびパッケージ内のファイルおよびリソースへのアクセスを提供します。 importlib.resources
の他の機能はhighlighted laterになります。
入力の機能強化
Type hints and annotationsは、Python3シリーズのリリース全体を通じて絶えず開発されています。 Pythonのタイピングシステムは現在非常に安定しています。 それでも、Python 3.7では、パフォーマンスの向上、コアサポート、前方参照など、いくつかの機能強化がテーブルに加えられています。
Pythonは、実行時に型チェックを行いません(enforce
などのパッケージを明示的に使用している場合を除く)。 したがって、タイプヒントをコードに追加しても、そのパフォーマンスに影響はありません。
残念ながら、ほとんどのタイプヒントにはtyping
モジュールが必要なため、これは完全には当てはまりません。 typing
モジュールは、標準ライブラリのslowest modulesの1つです。 PEP 560は、Python 3.7で入力するためのコアサポートを追加します。これにより、typing
モジュールが大幅に高速化されます。 この詳細は、一般に知る必要はありません。 後ろに傾いて、パフォーマンスの向上をお楽しみください。
Pythonの型システムは適度に表現力がありますが、多少の痛みを引き起こす問題の1つは前方参照です。 型のヒント(より一般的には注釈)は、モジュールのインポート中に評価されます。 したがって、使用する前にすべての名前を定義しておく必要があります。 以下は不可能です。
class Tree:
def __init__(self, left: Tree, right: Tree) -> None:
self.left = left
self.right = right
クラスTree
が.__init__()
メソッドの定義でまだ(完全に)定義されていないため、コードを実行するとNameError
が発生します。
Traceback (most recent call last):
File "tree.py", line 1, in
class Tree:
File "tree.py", line 2, in Tree
def __init__(self, left: Tree, right: Tree) -> None:
NameError: name 'Tree' is not defined
これを克服するには、代わりに"Tree"
を文字列リテラルとして記述する必要があります。
class Tree:
def __init__(self, left: "Tree", right: "Tree") -> None:
self.left = left
self.right = right
元の説明については、PEP 484を参照してください。
将来のPython 4.0では、このようないわゆる前方参照が許可される予定です。 これは、明示的に要求されるまで注釈を評価しないことで処理されます。 PEP 563は、この提案の詳細を説明しています。 Python 3.7では、前方参照はすでにhttps://docs.python.org/library/future.html [__future__
import]として使用できます。 次のように記述できます。
from __future__ import annotations
class Tree:
def __init__(self, left: Tree, right: Tree) -> None:
self.left = left
self.right = right
やや不器用な"Tree"
構文を回避することに加えて、型ヒントが実行されないため、注釈の評価を延期するとコードが高速化されることに注意してください。 前方参照はすでにmypy
でサポートされています。
注釈の最も一般的な使用法は、タイプヒンティングです。 それでも、実行時に注釈に完全にアクセスでき、必要に応じて使用できます。 アノテーションを直接処理する場合は、可能な前方参照を明示的に処理する必要があります。
注釈が評価されるタイミングを示す、明らかに馬鹿げた例を作成してみましょう。 まず、古いスタイルで行うため、注釈はインポート時に評価されます。 anno.py
に次のコードが含まれているとします。
def greet(name: print("Now!")):
print(f"Hello {name}")
name
の注釈はprint()
であることに注意してください。 これは、注釈が評価されるタイミングを正確に確認するためです。 新しいモジュールをインポートします。
>>>
>>> import anno
Now!
>>> anno.greet.__annotations__
{'name': None}
>>> anno.greet("Alice")
Hello Alice
ご覧のとおり、注釈はインポート時に評価されました。 name
はprint()
の戻り値であるため、最終的にNone
で注釈が付けられることに注意してください。
__future__
インポートを追加して、注釈の延期された評価を有効にします。
from __future__ import annotations
def greet(name: print("Now!")):
print(f"Hello {name}")
この更新されたコードをインポートしても、注釈は評価されません。
>>>
>>> import anno
>>> anno.greet.__annotations__
{'name': "print('Now!')"}
>>> anno.greet("Marty")
Hello Marty
Now!
は出力されず、注釈は__annotations__
ディクショナリに文字列リテラルとして保持されることに注意してください。 注釈を評価するには、typing.get_type_hints()
またはeval()
を使用します。
>>>
>>> import typing
>>> typing.get_type_hints(anno.greet)
Now!
{'name': }
>>> eval(anno.greet.__annotations__["name"])
Now!
>>> anno.greet.__annotations__
{'name': "print('Now!')"}
__annotations__
ディクショナリは決して更新されないため、使用するたびに注釈を評価する必要があることに注意してください。
タイミング精度
-
clock_gettime_ns()
:指定された時計の時刻を返します -
clock_settime_ns()
:指定した時計の時刻を設定します -
monotonic_ns()
:(たとえば夏時間のために)逆行できない相対時計の時刻を返します。 -
perf_counter_ns()
:パフォーマンスカウンターの値を返します—短い間隔を測定するために特別に設計されたクロック -
process_time_ns()
:現在のプロセスのシステムとユーザーのCPU時間の合計を返します(スリープ時間を含まない) -
time_ns()
:1970年1月1日からのナノ秒数を返します
ある意味では、追加された新しい機能はありません。 各関数は、_ns
サフィックスのない既存の関数に似ています。 違いは、新しい関数がfloat
として秒数ではなく、int
としてナノ秒数を返すことです。
ほとんどのアプリケーションでは、これらの新しいナノ秒機能と古い同等の機能との違いはあまり認識されません。 ただし、新しい関数はfloat
ではなくint
に依存しているため、推論が容易です。 浮動小数点数はby nature inaccurateです:
>>>
>>> 0.1 + 0.1 + 0.1
0.30000000000000004
>>> 0.1 + 0.1 + 0.1 == 0.3
False
これはPythonの問題ではなく、有限のビット数を使用して無限の10進数を表現する必要があるコンピューターの結果です。
Pythonfloat
はIEEE 754 standardの後に続き、53の有効ビットを使用します。 その結果、約104日(2⁵³または約9 quadrillion nanoseconds)を超える時間は、ナノ秒の精度でフロートとして表現できません。 対照的に、Pythonはint
is unlimitedであるため、整数のナノ秒数は、時間値に関係なく常にナノ秒の精度を持ちます。
例として、time.time()
は1970年1月1日からの秒数を返します。 この数値はすでに非常に大きいため、この数値の精度はマイクロ秒レベルです。 この関数は、_ns
バージョンで最大の改善を示している関数です。 time.time_ns()
の解像度は、time.time()
よりも約3 times betterです。
ところでナノ秒とは何ですか? 技術的には、10億分の1秒、または科学的記数法を好む場合は1e-9
秒です。 ただし、これらは単なる数字であり、実際には直感を提供しません。 より良い視覚補助については、Grace Hopper’s素晴らしいdemonstration of the nanosecondを参照してください。
余談ですが、ナノ秒の精度で日時を処理する必要がある場合、datetime
標準ライブラリはそれをカットしません。 明示的にはマイクロ秒のみを処理します。
>>>
>>> from datetime import datetime, timedelta
>>> datetime(2018, 6, 27) + timedelta(seconds=1e-6)
datetime.datetime(2018, 6, 27, 0, 0, 0, 1)
>>> datetime(2018, 6, 27) + timedelta(seconds=1e-9)
datetime.datetime(2018, 6, 27, 0, 0)
代わりに、astropy
projectを使用できます。 そのastropy.time
パッケージは、2つのfloat
オブジェクトを使用して日時を表し、「宇宙の年齢にまたがる時間にわたってサブナノ秒の精度」を保証します。
>>>
>>> from astropy.time import Time, TimeDelta
>>> Time("2018-06-27")
astropy
の最新バージョンは、Python3.5以降で利用できます。
その他のかなりクールな機能
これまで、Python 3.7の新機能に関するヘッドラインニュースを見てきました。 ただし、他にも多くの変更点があり、それらもかなりクールです。 このセクションでは、それらのいくつかを簡単に見ていきます。
辞書の順序は保証されています
Python 3.6のCPython実装では、辞書を並べています。 (PyPyにもこれがあります。)これは、辞書内のアイテムが挿入されたのと同じ順序で繰り返されることを意味します。 最初の例はPython 3.5を使用し、2番目の例はPython 3.6を使用しています。
>>>
>>> {"one": 1, "two": 2, "three": 3} # Python <= 3.5
{'three': 3, 'one': 1, 'two': 2}
>>> {"one": 1, "two": 2, "three": 3} # Python >= 3.6
{'one': 1, 'two': 2, 'three': 3}
Python 3.6では、この順序付けは、dict
の実装の良い結果でした。 ただし、Python 3.7では、挿入順序を保持する辞書はlanguage specificationの一部です。 そのため、Python> = 3.7(またはCPython> = 3.6)のみをサポートするプロジェクトに依存するようになりました。
「async
」と「await
」はキーワードです
Python 3.5ではcoroutines with async
and await
syntaxが導入されました。 下位互換性の問題を回避するために、async
とawait
は予約済みキーワードのリストに追加されませんでした。 つまり、async
およびawait
という名前の変数または関数を定義することは可能でした。
Python 3.7では、これはもはや不可能です。
>>>
>>> async = 1
File "", line 1
async = 1
^
SyntaxError: invalid syntax
>>> def await():
File "", line 1
def await():
^
SyntaxError: invalid syntax
「asyncio
」フェイスリフト
asyncio
標準ライブラリは元々Python 3.4で導入され、イベントループ、コルーチン、先物を使用して最新の方法で同時実行を処理します。 これがgentle introductionです。
Python 3.7では、asyncio
モジュールはmajor face liftを取得しています。これには、多くの新しい関数、コンテキスト変数のサポート(belowを参照)、およびパフォーマンスの向上が含まれます。 特に注目すべきはasyncio.run()
です。これは、同期コードからのコルーチンの呼び出しを簡素化します。 asyncio.run()
を使用すると、イベントループを明示的に作成する必要はありません。 非同期Hello Worldプログラムを作成できるようになりました。
import asyncio
async def hello_world():
print("Hello World!")
asyncio.run(hello_world())
コンテキスト変数
コンテキスト変数は、コンテキストに応じて異なる値を持つことができる変数です。 これらは、各実行スレッドが変数に対して異なる値を持つ可能性があるスレッドローカルストレージに似ています。 ただし、コンテキスト変数では、1つの実行スレッドに複数のコンテキストが存在する場合があります。 コンテキスト変数の主な使用例は、同時非同期タスクの変数を追跡することです。
次の例では、3つのコンテキストを作成し、それぞれに値name
の独自の値を設定します。 greet()
関数は、後で各コンテキスト内でname
の値を使用できるようになります。
import contextvars
name = contextvars.ContextVar("name")
contexts = list()
def greet():
print(f"Hello {name.get()}")
# Construct contexts and set the context variable name
for first_name in ["Steve", "Dina", "Harry"]:
ctx = contextvars.copy_context()
ctx.run(name.set, first_name)
contexts.append(ctx)
# Run greet function inside each context
for ctx in reversed(contexts):
ctx.run(greet)
このスクリプトを実行すると、Steve、Dina、およびHarryの順序が逆になります。
$ python3.7 context_demo.py
Hello Harry
Hello Dina
Hello Steve
「importlib.resources
」を使用してデータファイルをインポートする
Pythonプロジェクトをパッケージ化するときの1つの課題は、プロジェクトに必要なデータファイルなどのプロジェクトリソースをどうするかを決定することです。 いくつかのオプションが一般的に使用されています:
-
データファイルへのパスをハードコードします。
-
データファイルをパッケージ内に配置し、
__file__
を使用して検索します。 -
setuptools.pkg_resources
を使用して、データファイルリソースにアクセスします。
これらにはそれぞれ欠点があります。 最初のオプションは移植性がありません。 __file__
を使用する方が移植性が高くなりますが、Pythonプロジェクトがインストールされている場合は、zip内に収まり、__file__
属性がない可能性があります。 3番目のオプションはこの問題を解決しますが、残念ながら非常に遅いです。
より良い解決策は、標準ライブラリの新しいimportlib.resources
モジュールです。 Pythonの既存のインポート機能を使用して、データファイルもインポートします。 次のようなPythonパッケージ内にリソースがあると仮定します。
data/ │ ├── alice_in_wonderland.txt └── __init__.py
data
はPython packageである必要があることに注意してください。 つまり、ディレクトリには__init__.py
ファイル(空の場合があります)が含まれている必要があります。 次に、次のようにalice_in_wonderland.txt
ファイルを読み取ることができます。
>>>
>>> from importlib import resources
>>> with resources.open_text("data", "alice_in_wonderland.txt") as fid:
... alice = fid.readlines()
...
>>> print("".join(alice[:7]))
CHAPTER I. Down the Rabbit-Hole
Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, "and what is the use of a book," thought Alice "without pictures or
conversations?"
同様のresources.open_binary()
関数は、バイナリモードでファイルを開くために使用できます。 以前の“plugins as module attributes” exampleでは、importlib.resources
を使用して、resources.contents()
を使用して使用可能なプラグインを検出していました。 詳細については、Barry Warsaw’s PyCon 2018 talkを参照してください。
backportを介してPython 2.7およびPython 3.4+でimportlib.resources
を使用することができます。 guide on migrating from pkg_resources
to importlib.resources
が利用可能です。
開発者の秘
Python 3.7には、開発者としてのユーザー向けの機能がいくつか追加されています。 already seen the new breakpoint()
built-inがあります。 さらに、いくつかの新しい-X
command line optionsがPythonインタープリターに追加されました。
-X importtime
を使用すると、スクリプトのインポートにかかる時間を簡単に把握できます。
$ python3.7 -X importtime my_script.py
import time: self [us] | cumulative | imported package
import time: 2607 | 2607 | _frozen_importlib_external
...
import time: 844 | 28866 | importlib.resources
import time: 404 | 30434 | plugins
cumulative
列には、インポートの累積時間(マイクロ秒単位)が表示されます。 この例では、plugins
のインポートに約0.03秒かかり、そのほとんどがimportlib.resources
のインポートに費やされました。 self
列には、ネストされたインポートを除いたインポート時間が表示されます。
これで、-X dev
を使用して「開発モード」をアクティブ化できます。開発モードでは、デフォルトで有効にするには遅すぎると見なされる特定のデバッグ機能とランタイムチェックが追加されます。 これには、faulthandler
が重大なクラッシュのトレースバックを表示できるようにすることや、警告やデバッグフックを増やすことが含まれます。
最後に、-X utf8
はUTF-8 modeを有効にします。 (PEP 540を参照してください。)このモードでは、現在のロケールに関係なく、UTF-8
がテキストのエンコードに使用されます。
最適化
Pythonの新しいリリースには、それぞれ最適化のセットが付属しています。 Python 3.7には、次のような大幅な高速化があります。
-
標準ライブラリの多くのメソッドを呼び出す際のオーバーヘッドが少なくなります。
-
メソッド呼び出しは一般に最大20%高速です。
-
Python自体の起動時間は10〜30%短縮されます。
-
typing
のインポートは7倍高速です。
さらに、より多くの特殊な最適化が含まれています。 詳細な概要については、this listを参照してください。
これらすべての最適化の結果は、Python 3.7 is fastです。 これは単にこれまでにリリースされたfastest version of CPythonです。
だから、アップグレードする必要がありますか?
簡単な答えから始めましょう。 ここで見た新機能を試してみたい場合は、Python 3.7を使用できる必要があります。 pyenv
やAnacondaなどのツールを使用すると、Pythonの複数のバージョンを簡単に並べてインストールできます。 Python 3.7をインストールして試してみてもマイナス面はありません。
さて、より複雑な質問に。 実稼働環境をPython 3.7にアップグレードする必要がありますか? 新しい機能を利用するには、Python 3.7に依存する独自のプロジェクトを作成する必要がありますか?
実稼働環境をアップグレードする前に常に徹底的なテストを行う必要があるという明らかな警告がありますが、Python 3.7には、以前のコードを壊すものはほとんどありません(ただし、async
とawait
がキーワードになることは一例です)。 すでに最新のPythonを使用している場合、3.7へのアップグレードは非常にスムーズに行われるはずです。 少し保守的になりたい場合は、最初のメンテナンスリリース(Python 3.7.1)のリリース(https://www.python.org/dev/peps/pep-0537/#maintenance-releases)を待つことをお勧めします。 [2018年7月に暫定的に予想される]。
プロジェクトを3.7のみにするべきだと主張するのは難しいです。 Python 3.7の新機能の多くは、Python 3.6へのバックポート(データクラス、importlib.resources
)または便利さ(より高速な起動とメソッド呼び出し、より簡単なデバッグ、および-X
オプション)として利用できます。 後者では、Python 3.6(またはそれ以下)との互換性を保ちながら、Python 3.7を自分で実行することで利用できます。
コードをPython3.7にロックする大きな機能は、__getattr__()
on modules、forward references in type hints、およびnanosecond time
functionsです。 これらのいずれかが本当に必要な場合は、先に進んで要件を改善する必要があります。 そうでなければ、あなたのプロジェクトは、Python 3.6でしばらく実行することができれば、おそらく他の人にとってより役に立つでしょう。
アップグレード時に注意すべき詳細については、Porting to Python 3.7 guideを参照してください。