Python 3.7の新機能

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

これを機能させるには、pudbpip 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__()には、次の手順が含まれています。 次のリストの番号は、コード内の番号付きコメントに対応しています。

  1. まず、関数は楽観的にPLUGINSディクショナリから名前付きプラグインを返そうとします。 これは、nameという名前のプラグインが存在し、すでにインポートされている場合に成功します。

  2. 名前付きプラグインがPLUGINSディクショナリに見つからない場合は、すべてのプラグインがインポートされていることを確認します。

  3. インポート後に名前付きプラグインが使用可能になった場合、そのプラグインを返します。

  4. すべてのプラグインをインポートした後、プラグインが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

ご覧のとおり、注釈はインポート時に評価されました。 nameprint()の戻り値であるため、最終的に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__ディクショナリは決して更新されないため、使用するたびに注釈を評価する必要があることに注意してください。

タイミング精度

Python 3.7では、timeモジュールは、PEP 564で説明されているようにいくつかの新しい関数を取得します。 特に、次の6つの機能が追加されています。

  • 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進数を表現する必要があるコンピューターの結果です。

PythonfloatIEEE 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が導入されました。 下位互換性の問題を回避するために、asyncawaitは予約済みキーワードのリストに追加されませんでした。 つまり、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

dataPython 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 utf8UTF-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を使用できる必要があります。 pyenvAnacondaなどのツールを使用すると、Pythonの複数のバージョンを簡単に並べてインストールできます。 Python 3.7をインストールして試してみてもマイナス面はありません。

さて、より複雑な質問に。 実稼働環境をPython 3.7にアップグレードする必要がありますか? 新しい機能を利用するには、Python 3.7に依存する独自のプロジェクトを作成する必要がありますか?

実稼働環境をアップグレードする前に常に徹底的なテストを行う必要があるという明らかな警告がありますが、Python 3.7には、以前のコードを壊すものはほとんどありません(ただし、asyncawaitがキーワードになることは一例です)。 すでに最新の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 modulesforward references in type hints、およびnanosecond time functionsです。 これらのいずれかが本当に必要な場合は、先に進んで要件を改善する必要があります。 そうでなければ、あなたのプロジェクトは、Python 3.6でしばらく実行することができれば、おそらく他の人にとってより役に立つでしょう。

アップグレード時に注意すべき詳細については、Porting to Python 3.7 guideを参照してください。