Web開発のための非同期Python

非同期プログラミングは、ファイルの読み書きを頻繁に行ったり、サーバーとデータのやり取りを行ったりするような作業に適しています。

非同期プログラムはI/O操作をノンブロッキングで行います。

つまり、クライアントからデータが戻ってくるのを待っている間に他の作業を行うことができるので、ぼんやり待って資源や時間を浪費することがありません。

Pythonは、他の多くの言語と同様に、デフォルトで非同期でないことに苦しんでいます。

幸いなことに、ITの世界の急速な変化により、元々非同期を想定していない言語を使っていても、非同期なコードを書くことができるようになりました。

長年にわたり、スピードへの要求はハードウェアの能力を超えており、この問題に対処するため、世界中の企業がReactive Manifestoに結集したのです。

非同期プログラムのノンブロッキング動作は、Webアプリケーションのコンテキストで大きなパフォーマンス上の利点をもたらし、リアクティブなアプリケーションの開発の問題点を解決するのに役立ちます。

Python 3には、非同期アプリケーションを記述するための強力なツールが組み込まれています。

この記事では、特にWeb開発に関連するこれらのツールのいくつかを取り上げます。

ユーザーの地理的な座標が与えられたときに、太陽系の惑星の現在の関連する空の座標を表示する、シンプルな反応型aiohttpベースのアプリケーションを開発します。

アプリはこちらで、ソースコードはこちらでご覧になれます。

最後に、このアプリをHerokuにデプロイするための準備方法について説明します。

非同期Python入門

従来のPythonのコードを書き慣れている人にとって、非同期コードにジャンプすることは概念的に少し難しいかもしれません。

Pythonの非同期コードはコルーチンに依存しており、イベントループと組み合わせることで、同時に複数のことを行っているように見えるコードを書くことができるようになります。

コルーチンは、プログラムの制御を呼び出し側のコンテキストに戻すポイントを持つ関数と考えることができます。

この「yield」ポイントにより、コルーチンの実行を一時停止したり再開したり、コンテキスト間でデータを交換したりすることができる。

イベントループは、任意の瞬間に実行されるコードの塊を決定し、コルーチン間の一時停止、再開、通信を担当する。

つまり、異なるコルーチンの一部が、スケジュールされた順序とは異なる順序で実行されることになるかもしれません。

このように、異なるコードの塊を順番通りに実行することを並行処理と呼びます。

HTTP` リクエストのコンテキストで同時実行について考えると、理解しやすいでしょう。

サーバに多くの独立したリクエストを行いたいと想像してください。

例えば、あるシーズンに活躍したすべてのスポーツ選手についての統計情報を得るために、あるウェブサイトに問い合わせをしたいかもしれません。

各リクエストを順番に実行することができます。

しかし、リクエストのたびに、コードがサーバーにリクエストが届くまで、そしてレスポンスが戻ってくるまで、待ち時間が発生することが想像できます。

時には、これらの処理に数秒かかることもあります。

また、ユーザー数が多い場合や、単にサーバーの速度制限により、アプリケーションにネットワーク遅延が発生することもあります。

もし、サーバーからの応答を待っている間、コードが他のことをすることができたらどうでしょうか?さらに、応答データが到着した時点で、与えられたリクエストの処理に戻ることができたらどうでしょう?もし、個々のリクエストの終了を待たずに次のリクエストに進むことができれば、多くのリクエストを連続して行うことができます。

イベントループを持つコルーチンでは、まさにこのような動作をするコードを書くことができます。

非同期

asyncio は Python 標準ライブラリの一部で、イベントループとそれを制御するためのツールのセットを提供します。

asyncio を使うと、コルーチンの実行をスケジュールしたり、構成するコルーチンが実行し終わったら初めて実行が終了する新しいコルーチン (asyncio の言い回しで言うと asyncio.Task オブジェクト) を作成したりすることができる。

他の非同期プログラミング言語とは異なり、Pythonは言語と一緒に提供されるイベントループを使うことを強制しません。

Brett Cannonが指摘するように、Pythonのコルーチンは非同期APIであり、任意のイベントループを使用することができます。

curioのように全く別のイベントループを実装するプロジェクトや、uvloopのようにasyncioのために別のイベントループポリシー(イベントループポリシーはイベントループを「裏側」で管理するもの)をドロップインすることができるプロジェクトも存在します。

ここでは、2つのコルーチンを同時に実行し、それぞれが1秒後にメッセージを出力するコードを見てみましょう。

# example1.py
import asyncio


async def wait_around(n, name):
    for i in range(n):
        print(f"{name}: iteration {i}")
        await asyncio.sleep(1.0)


async def main():
    await asyncio.gather(*[
        wait_around(2, "coroutine 0"), wait_around(5, "coroutine 1")
    ])


loop = asyncio.get_event_loop()
loop.run_until_complete(main())


me@local:~$ time python example1.py
coroutine 1: iteration 0
coroutine 0: iteration 0
coroutine 1: iteration 1
coroutine 0: iteration 1
coroutine 1: iteration 2
coroutine 1: iteration 3
coroutine 1: iteration 4


real    0m5.138s
user    0m0.111s
sys     0m0.019s


このコードはおよそ5秒で実行されます。

これは asyncio.sleep コルーチンが、イベントループが他のコードを実行するためにジャンプできるポイントを確立しているからです。

さらに、イベントループは asyncio.gather 関数で wait_around インスタンスを同時に実行するようスケジュールするように指示しました。

asyncio.gatherは "awaitables" (コルーチン、またはasyncio.Taskオブジェクト) のリストを受け取り、すべてのタスク/コルーチンが終了したときにのみ終了する単一のasyncio.Taskオブジェクトを返します。

最後の2行は、与えられたコルーチンを実行が終了するまで実行するためのasyncio` ボイラープレートである。

コルーチンは関数と違って、呼び出されたらすぐに実行を開始するわけではありません。

await` キーワードはイベントループにコルーチンの実行をスケジュールするように指示するものです。

asyncio.sleepの前にあるawait` を取り除くと、プログラムは(ほぼ)即座に終了します。

なぜなら、イベントループに実際にコルーチンを実行するように指示しておらず、この場合はコルーチンに一定時間、一時停止するように指示しているからです。

非同期Pythonコードがどのようなものかを理解した上で、非同期Web開発に進みましょう。

aiohttpのインストール

aiohttp は非同期の HTTP リクエストを行うための Python ライブラリです。

さらに、Webアプリケーションのサーバ部分をまとめるためのフレームワークも提供します。

Python 3.5+ と pip を使って、aiohttp をインストールすることができます。

pip install --user aiohttp


Client-Side: リクエストの作成

次の例は、aiohttp を使って “example.com” ウェブサイトの HTML コンテンツをダウンロードする方法を示しています。

# example2_basic_aiohttp_request.py
import asyncio
import aiohttp


async def make_request():
    url = "https://example.com"
    print(f"making request to {url}")
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            if resp.status == 200:
                print(await resp.text())


loop = asyncio.get_event_loop()
loop.run_until_complete(make_request())


強調すべき点がいくつかあります。

  • asyncio.sleepと同様に、ページの HTML コンテンツを取得するためにはresp.text()await` を使用しなければなりません。もしこれを省くと、プログラムの出力は以下のようなものになります。
me@local:~$ python example2_basic_aiohttp_request.py
<coroutine 0x7fe64e574ba0="" at="" clientresponse.text="" object=""


  • async with`は、関数の代わりにコルーチンで動作するコンテキストマネージャーです。これが使用される両方のケースで、内部的には aiohttp がサーバへの接続を閉じたり、リソースを解放していることが想像できます。
  • aiohttp.ClientSession は HTTP verbs に対応するメソッドを持っています。同じように

のように、 session.get は GET リクエストを行い、 session.post は POST リクエストを行います。

この例自体は、同期 HTTP リクエストを作成することによるパフォーマンス上の利点はありません。

クライアントサイドの aiohttp の本当の美しさは、複数のリクエストを同時に行うことにあります。

# example3_multiple_aiohttp_request.py
import asyncio
import aiohttp


async def make_request(session, req_n):
    url = "https://example.com"
    print(f"making request {req_n} to {url}")
    async with session.get(url) as resp:
        if resp.status == 200:
            await resp.text()


async def main():
    n_requests = 100
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(
            *[make_request(session, i) for i in range(n_requests)]
        )


loop = asyncio.get_event_loop()
loop.run_until_complete(main())


各リクエストを順番に行うのではなく、 asyncioasycio.gather で同時並行に行うように依頼します。

PlanetTracker Web App

このセクションでは、ユーザーがいる場所の空にある惑星の現在の座標(エフェメライド)を報告するアプリを作成する方法を紹介します。

ユーザーが自分の位置を提供すると、WebのGeolocation APIがそれを代行してくれます。

最後に、このアプリをHerokuにデプロイするためにProcfileを設定する方法を紹介します。

Python 3.6とpipがインストールされていると仮定して、以下のようにします。

me@local:~$ mkdir planettracker &amp;&amp; cd planettracker
me@local:~/planettracker$ pip install --user pipenv
me@local:~/planettracker$ pipenv --python=3


PyEphemによる惑星エフェメリデスの作成

天体のエフェメリスとは、地球上のある場所と時間における天空の現在位置のことです。

PyEphemはエフェメリスを正確に計算するためのPythonライブラリです。

一般的な天文オブジェクトがライブラリに組み込まれているので、特にこのタスクに適しています。

まず、PyEphemをインストールしましょう。

me@local:~/planettracker$ pipenv install ephem


火星の現在の座標を得るのは、Observerクラスのインスタンスを使って座標を compute するのと同じくらい簡単です。

import ephem
import math
convert = math.pi / 180.
mars = ephem.Mars()
greenwich = ephem.Observer()
greenwich.lat = "51.4769"
greenwich.lon = "-0.0005"
mars.compute(observer)
az_deg, alt_deg = mars.az*convert, mars.alt*convert
print(f"Mars' current azimuth and elevation: {az_deg:.2f} {alt_deg:.2f}")


惑星のエフェメライドを簡単に取得するために、与えられた惑星の現在の方位と高度を度単位で返すメソッドを持つ PlanetTracker クラスをセットアップしましょう(PyEphemのデフォルトでは、内部で角度を表すのに度ではなくラジアンを使用します)。

# planet_tracker.py
import math
import ephem


class PlanetTracker(ephem.Observer):


def __init__(self):
        super(PlanetTracker, self).__init__()
        self.planets = {
            "mercury": ephem.Mercury(),
            "venus": ephem.Venus(),
            "mars": ephem.Mars(),
            "jupiter": ephem.Jupiter(),
            "saturn": ephem.Saturn(),
            "uranus": ephem.Uranus(),
            "neptune": ephem.Neptune()
        }


def calc_planet(self, planet_name, when=None):
        convert = 180./math.pi
        if when is None:
            when = ephem.now()


self.date = when
        if planet_name in self.planets:
            planet = self.planets[planet_name]
            planet.compute(self)
            return {
                "az": float(planet.az)*convert,
                "alt": float(planet.alt)*convert,
                "name": planet_name
            }
        else:
            raise KeyError(f"Couldn't find {planet_name} in planets dict")


これで、太陽系にある他の7つの惑星のどれかを簡単に取得できます。

from planet_tracker import PlanetTracker
tracker = PlanetTracker()
tracker.lat = "51.4769"
tracker.lon = "-0.0005"
tracker.calc_planet("mars")


このコードを実行すると、次のようになります。

{'az': 92.90019644871396, 'alt': -23.146670983905302, 'name': 'mars'}


サーバーサイドの aiohttp: HTTP ルーティング

ある緯度と経度が与えられれば、惑星の現在のエフェメリスを簡単に度単位で得ることができます。

では、クライアントがユーザーの地理的位置を指定して惑星のエフェメリスを取得できるように、aiohttpルートをセットアップしてみましょう。

コードを書き始める前に、どのHTTP動詞をそれぞれのタスクに関連付けるかを考える必要があります。

最初のタスクでは、観測者の地理座標を設定するため、POST を使用することは理にかなっています。

エフェメライドを取得するのであれば、2番目のタスクにGETを使用するのは理にかなっています。

# aiohttp_app.py
from aiohttp import web


from planet_tracker import PlanetTracker


@routes.get("/planets/{name}")
async def get_planet_ephmeris(request):
    planet_name = request.match_info['name']
    data = request.query
    try:
        geo_location_data = {
            "lon": str(data["lon"]),
            "lat": str(data["lat"]),
            "elevation": float(data["elevation"])
        }
    except KeyError as err:
        # default to Greenwich Observatory
        geo_location_data = {
            "lon": "-0.0005",
            "lat": "51.4769",
            "elevation": 0.0,
        }
    print(f"get_planet_ephmeris: {planet_name}, {geo_location_data}")
    tracker = PlanetTracker()
    tracker.lon = geo_location_data["lon"]
    tracker.lat = geo_location_data["lat"]
    tracker.elevation = geo_location_data["elevation"]
    planet_data = tracker.calc_planet(planet_name)
    return web.json_response(planet_data)


app = web.Application()
app.add_routes(routes)


web.run_app(app, host="localhost", port=8000)


ここで、route.getデコレーターは、 get_planet_ephmeris コルーチンが変数 GET ルートのハンドラーであることを表しています。

これを実行する前に、pipenvでaiohttpをインストールしましょう。

me@local:~/planettracker$ pipenv install aiohttp


これでアプリを実行することができます。

me@local:~/planettracker$ pipenv run python aiohttp_app.py


このアプリを実行すると、ブラウザで異なるルートを指定して、サーバーが返すデータを見ることができます。

ブラウザのアドレスバーに localhost:8000/planets/mars と入力すると、以下のようなレスポンスが表示されるはずです。

{"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"}


これは、以下のcurlコマンドを発行したのと同じです。

me@local:~$ curl localhost:8000/planets/mars
{"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"}


もしあなたがcurlに慣れていないなら、それはHTTPルートをテストするための便利なコマンドラインツールです。

GET URLをcurlに渡すことができます。

me@local:~$ curl localhost:8000/planets/mars
{"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"}


これは、イギリスのグリニッジ天文台にある火星のエフェメリス(天体暦)を示しています。

GET`リクエストのURLに座標をエンコードすることで、他の場所のMars’ ephemerisを取得することができます(URLの周りに引用符があることに注意してください)。

me@local:~$ curl "localhost:8000/planets/mars?lon=145.051⪫=-39.754&amp;elevation=0"
{"az": 102.30273048280189, "alt": 11.690380174890928, "name": "mars"


curl` は POST リクエストにも使用することができます。

me@local:~$ curl --header "Content-Type: application/x-www-form-urlencoded" --data "lat=48.93&amp;lon=2.45&amp;elevation=0" localhost:8000/geo_location
{"lon": "2.45", "lat": "48.93", "elevation": 0.0}


dataフィールドを指定することで、curl` は自動的に POST リクエストを行うことを想定していることに注意してください。

次に進む前に、web.run_app関数がブロッキング方式でアプリを実行することに注意してください。

同時に実行するためには、もう少しコードを追加する必要があります。

# aiohttp_app.py
import asyncio
...


# web.run_app(app)


async def start_app():
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(
        runner, parsed.host, parsed.port)
    await site.start()
    print(f"Serving up app on {parsed.host}:{parsed.port}")
    return runner, site


loop = asyncio.get_event_loop()
runner, site = loop.run_until_complete(start_async_app())
try:
    loop.run_forever()
except KeyboardInterrupt as err:
    loop.run_until_complete(runner.cleanup())


先ほど見た loop.run_until_complete の代わりに loop.run_forever が存在することに注意してください。

このプログラムでは、決められた数のコルーチンを実行するのではなく、 ctrl+c で終了するまでリクエストを処理するサーバーを起動し、その時点でサーバーを緩やかにシャットダウンするようにしたいのです。

HTML/JavaScript クライアント

aiohttp を使用すると、HTML や JavaScript ファイルを提供することができます。

CSSやJavaScriptのような “静的 “なアセットを提供するためにaiohttpを使用することは推奨されませんが、このアプリの目的では問題ないでしょう。

JavaScriptファイルを参照するHTMLファイルを提供するために、aiohttp_app.pyファイルに数行追加してみましょう。

# aiohttp_app.py
...
@routes.get('/')
async def hello(request):
    return web.FileResponse("./index.html")


app = web.Application()
app.add_routes(routes)
app.router.add_static("/", "./")
...


helloコルーチンはlocalhost:8000/に GET ルートを設定し、サーバを実行しているのと同じディレクトリにあるindex.html` のコンテンツを提供します。

app.router.add_staticの行は、localhost:8000/にルートを設定し、サーバーを実行するのと同じディレクトリにあるファイルを提供します。

これは、ブラウザがindex.html` で参照している JavaScript ファイルを見つけることができることを意味します。

注意: 実稼働環境では、HTML、CSS、JSファイルを別のディレクトリに移動し、そのディレクトリでサービスを提供することが理にかなっています。

こうすることで、好奇心旺盛なユーザーが私たちのサーバーコードにアクセスできないようにします。

HTMLファイルは非常にシンプルです。

<!DOCTYPE html

<html lang="en"
<head
<meta charset="utf-8"/
<meta content="width=device-width, initial-scale=1" name="viewport"/
<titlePlanet Tracker</title
</head
<body
<div id="app"
<label id="lon"Longitude: <input type="text"/</label<br/
<label id="lat"Latitude: <input type="text"/</label<br/
<label id="elevation"Elevation: <input type="text"/</label<br/
</div
<script src="/app.js"</script
</body


しかし、JavaScriptファイルはもう少し複雑です。

var App = function() {


this.planetNames = [
        "mercury",
        "venus",
        "mars",
        "jupiter",
        "saturn",
        "uranus",
        "neptune"
    ]


this.geoLocationIds = [
        "lon",
        "lat",
        "elevation"
    ]


this.keyUpInterval = 500
    this.keyUpTimer = null
    this.planetDisplayCreated = false
    this.updateInterval = 2000 // update very second and a half
    this.updateTimer = null
    this.geoLocation = null


this.init = function() {
        this.getGeoLocation().then((position) =&gt; {
            var coords = this.processCoordinates(position)
            this.geoLocation = coords
            this.initGeoLocationDisplay()
            this.updateGeoLocationDisplay()
            return this.getPlanetEphemerides()
        }).then((planetData) =&gt; {
            this.createPlanetDisplay()
            this.updatePlanetDisplay(planetData)
        }).then(() =&gt; {
            return this.initUpdateTimer()
        })
    }


this.update = function() {
        if (this.planetDisplayCreated) {
            this.getPlanetEphemerides().then((planetData) =&gt; {
                this.updatePlanetDisplay(planetData)
            })
        }
    }


this.get = function(url, data) {
        var request = new XMLHttpRequest()
        if (data !== undefined) {
            url += `?${data}`
        }
        // console.log(`get: ${url}`)
        request.open("GET", url, true)
        return new Promise((resolve, reject) =&gt; {
            request.send()
            request.onreadystatechange = function(){
                if (this.readyState === XMLHttpRequest.DONE &amp;&amp; this.status === 200) {
                    resolve(this)
                }
            }
            request.onerror = reject
        })
    }


this.processCoordinates = function(position) {
        var coordMap = {
            'longitude': 'lon',
            'latitude': 'lat',
            'altitude': 'elevation'
        }
        var coords = Object.keys(coordMap).reduce((obj, name) =&gt; {
            var coord = position.coords[name]
            if (coord === null || isNaN(coord)) {
                coord = 0.0
            }
            obj[coordMap[name]] = coord
            return obj
        }, {})
        return coords
    }


this.coordDataUrl = function (coords) {
        postUrl = Object.keys(coords).map((c) =&gt; {
            return `${c}=${coords[c]}`
        })
        return postUrl
    }


this.getGeoLocation = function() {
        return new Promise((resolve, reject) =&gt; {
            navigator.geolocation.getCurrentPosition(resolve)
        })
    }


this.getPlanetEphemeris = function(planetName) {
        var postUrlArr = this.coordDataUrl(this.geoLocation)
        return this.get(`/planets/${planetName}`, postUrlArr.join("&amp;")).then((req) =&gt; {
            return JSON.parse(req.response)
        })
    }


this.getPlanetEphemerides = function() {
        return Promise.all(
            this.planetNames.map((name) =&gt; {
                return this.getPlanetEphemeris(name)
            })
        )
    }


this.createPlanetDisplay = function() {
        var div = document.getElementById("app")
        var table = document.createElement("table")
        var header = document.createElement("tr")
        var headerNames = ["Name", "Azimuth", "Altitude"]
        headerNames.forEach((headerName) =&gt; {
            var headerElement = document.createElement("th")
            headerElement.textContent = headerName
            header.appendChild(headerElement)
        })
        table.appendChild(header)
        this.planetNames.forEach((name) =&gt; {
            var planetRow = document.createElement("tr")
            headerNames.forEach((headerName) =&gt; {
                planetRow.appendChild(
                    document.createElement("td")
                )
            })
            planetRow.setAttribute("id", name)
            table.appendChild(planetRow)
        })
        div.appendChild(table)
        this.planetDisplayCreated = true
    }


this.updatePlanetDisplay = function(planetData) {
        planetData.forEach((d) =&gt; {
            var content = [d.name, d.az, d.alt]
            var planetRow = document.getElementById(d.name)
            planetRow.childNodes.forEach((node, idx) =&gt; {
                var contentFloat = parseFloat(content[idx])
                if (isNaN(contentFloat)) {
                    node.textContent = content[idx]
                } else {
                    node.textContent = contentFloat.toFixed(2)
                }
            })
        })
    }


this.initGeoLocationDisplay = function() {
        this.geoLocationIds.forEach((id) =&gt; {
            var node = document.getElementById(id)
            node.childNodes[1].onkeyup = this.onGeoLocationKeyUp()
        })
        var appNode = document.getElementById("app")
        var resetLocationButton = document.createElement("button")
        resetLocationButton.setAttribute("id", "reset-location")
        resetLocationButton.onclick = this.onResetLocationClick()
        resetLocationButton.textContent = "Reset Geo Location"
        appNode.appendChild(resetLocationButton)
    }


this.updateGeoLocationDisplay = function() {
        Object.keys(this.geoLocation).forEach((id) =&gt; {
            var node = document.getElementById(id)
            node.childNodes[1].value = parseFloat(
                this.geoLocation[id]
            ).toFixed(2)
        })
    }


this.getDisplayedGeoLocation = function() {
        var displayedGeoLocation = this.geoLocationIds.reduce((val, id) =&gt; {
            var node = document.getElementById(id)
            var nodeVal = parseFloat(node.childNodes[1].value)
            val[id] = nodeVal
            if (isNaN(nodeVal)) {
                val.valid = false
            }
            return val
        }, {valid: true})
        return displayedGeoLocation
    }


this.onGeoLocationKeyUp = function() {
        return (evt) =&gt; {
            // console.log(evt.key, evt.code)
            var currentTime = new Date()
            if (this.keyUpTimer !== null){
                clearTimeout(this.keyUpTimer)
            }
            this.keyUpTimer = setTimeout(() =&gt; {
                var displayedGeoLocation = this.getDisplayedGeoLocation()
                if (displayedGeoLocation.valid) {
                    delete displayedGeoLocation.valid
                    this.geoLocation = displayedGeoLocation
                    console.log("Using user supplied geo location")
                }
            }, this.keyUpInterval)
        }
    }


this.onResetLocationClick = function() {
        return (evt) =&gt; {
            console.log("Geo location reset clicked")
            this.getGeoLocation().then((coords) =&gt; {
                this.geoLocation = this.processCoordinates(coords)
                this.updateGeoLocationDisplay()
            })
        }
    }


this.initUpdateTimer = function () {
        if (this.updateTimer !== null) {
            clearInterval(this.updateTimer)
        }
        this.updateTimer = setInterval(
            this.update.bind(this),
            this.updateInterval
        )
        return this.updateTimer
    }


this.testPerformance = function(n) {
        var t0 = performance.now()
        var promises = []
        for (var i=0; i<n; i++)="" promise.all(promises).then(()="" promises.push(this.getplanetephemeris("mars"))="" {="" }="" {
            var delta = (performance.now() - t0)/1000
            console.log(`Took ${delta.toFixed(4)} seconds to do ${n} requests`)
        })
    }
}


var app
document.addEventListener("DOMContentLoaded", (evt) =&gt; {
    app = new App()
    app.init()
})


このアプリは、定期的に(2秒ごとに)惑星エフェメライドを更新して表示します。

このアプリは定期的(2秒ごと)に惑星エフェメライドを更新して表示します。

自分自身の地理座標を提供するか、Web Geolocation API に現在の位置を決定させることができます。

このアプリは、ユーザーが半秒以上入力を止めるとジオロケーションを更新します。

これは JavaScript のチュートリアルではありませんが、スクリプトのさまざまな部分が何を行っているかを理解することは有用だと思います。

  • createPlanetDisplay は動的に HTML 要素を作成し、それらを Document Object Model (DOM) にバインドしています。
  • updatePlanetDisplay はサーバから受け取ったデータを受け取り、 createPlanetDisplay で作成された要素にデータを入力します。
  • get はサーバへの GET リクエストを行います。XMLHttpRequest オブジェクトを使用すると、ページを再読み込みすることなく、この処理を行うことができます。
  • post はサーバに POST リクエストを行います。get` と同様に、ページを再読み込みすることなく実行することができます。
  • getGeoLocation は Web Geolocation API を使用して、ユーザーの現在の地理的な座標を取得します。これは “安全なコンテキスト” で実行されなければなりません (つまり、 HTTP ではなく HTTPS を使用しなければなりません)。
  • getPlanetEphemerisgetPlanetEphemerides はそれぞれ、特定の惑星のエフェメリスとすべての惑星のエフェメリスを取得するためにサーバーに GET リクエストを送信します。
  • testPerformance はサーバーに n リクエストを行い、それにかかる時間を測定します。

Herokuへのデプロイの入門編

Herokuは、Webアプリケーションを簡単にデプロイするためのサービスです。

Herokuはリバースプロキシの設定やロードバランシングの心配など、アプリケーションのWeb向けのコンポーネントを設定することを引き受けます。

少数のリクエストと少数のユーザーを扱うアプリケーションのために、Herokuは素晴らしい無料のホスティングサービスです。

HerokuへのPythonアプリケーションのデプロイは、近年とても簡単になりました。

その核となるのは、アプリケーションの依存関係をリストアップするファイルと、アプリケーションをどのように実行するかをHerokuに伝えるファイルの2つを作成する必要があるということです。

Pipfileは前者の世話をし、Procfileは後者の世話をします。

Pipfile は pipenv を使って管理します。

依存関係をインストールするたびに Pipfile (と Pipfile.lock) に追加していきます。

Heroku上でアプリを動作させるためには、もう一つ依存関係を追加する必要があります。

me@local:~/planettracker$ pipenv install gunicorn


Procfileに以下の行を追加して、独自のProcfileを作成することができます。

web: gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker


基本的にこれは、特別なaiohttpウェブワーカーを使用して、アプリを実行するためにGunicornを使用するようにHerokuに指示するものです。

Heroku にデプロイする前に、Git でアプリの追跡を開始する必要があります。

me@local:~/planettracker$ git init
me@local:~/planettracker$ git add .
me@local:~/planettracker$ git commit -m "first commit"


あとは、Heroku devcenterにある指示に従ってアプリをデプロイしてください。

なお、このチュートリアルの「アプリを準備する」ステップは、すでにgitでトラッキングされたアプリを持っているので、省略することができます。

アプリケーションのデプロイが完了したら、ブラウザで指定したHerokuのURLに移動してアプリを表示すると、以下のようになります。

結論

この記事では、Pythonによる非同期Web開発がどのようなものか、その利点と用途について掘り下げました。

その後、ユーザーの地理的な座標が与えられると、太陽系の惑星の現在の関連する空の座標を動的に表示する、シンプルな反応型 aiohttp ベースのアプリケーションを構築しました。

このアプリケーションを構築した後、Herokuにデプロイするための準備をしました。

前述したように、必要に応じてソースコードとアプリケーションのデモの両方を見つけることができます。

;>

タイトルとURLをコピーしました