Pythonを勉強していて「こういう書き方があったのか」と思ったことを3つ

プログラミングを勉強していると
「こういう時はこんな書き方ができるのか」
とか
「この書き方を知っていればあの時のコードもっとうまく書けたな」
と感じることがあると思います。
この記事では私がPythonを勉強していてそう感じたことを3つ紹介したいと思います。
(常識だよ!ってことばかりだったらすみません)

1.複数の変数を比較する条件式の書き方

例えば3つの変数a, b, cがすべてNoneであれば真となる条件式は通常

if a is None and b is None and c is None:
    print u'すべてNone'

のように書きますが次のように書くこともできます。

if a is b is c is None:
    print u'すべてNone'

これはPythonの比較演算子の特徴を利用した書き方です。
Pythonでは変数aが1より大きく5より小さいという条件式を書く場合に下のように続けて書くことができます。

if 1 < a < 5:
    print u'aは1から5の間'

これを一般化すると「Pythonの比較演算子には右結合・左結合といった概念がなく常に両隣の値を比較し、その論理積が式の値となる」と考えることができます。そのため

1 < a < 5
# 下のように解釈される
1 < a and a < 5

と同様に

a is b is c is None
# 下に等しい
a is b and b is c and c is None

となり

a is None and b is None and c is None

と同値であることがわかります。

ただしこの書き方は否定が入るととたんに分かりづらくなってしまいます。
変数aとbがNoneでcがNoneではないという式は

a is b is None is not c

と書くことができますが読みづらいので効果的に使えるのは「すべての変数が同じ値である」などいくつかの条件の場合に限られるかと思います。

2.in演算子を使って変数が定義されているかどうかを確認する書き方

Pythonでは未定義の変数を参照しようとすると'NameError'例外となるため変数が定義されているかどうかを確認するにはこの例外をチェックするのが手っ取り早い方法です。
例えば変数hogeが定義されていればそのまま、定義されていなければ1を代入する場合は下のようになります。

try:
    hoge = hoge
except NameError:
    hoge = 1

ただ、変数が定義済みかどうかを確認するためだけに4行も使うのはちょっと大袈裟といいますかもっと簡潔に書きたいところです。
そこで使うのがin演算子と、定義されている変数の辞書を返す'locals()'、'globals()'関数です。
'locals()'関数は現在のスコープで定義されている変数を{'変数名': '値'}の辞書で返し'globals()'はグローバル変数の辞書を返します。
これを使って上のコードを書き直すと次のようになります。

hoge = hoge if 'hoge' in locals() else 1

参考:python - How do I check if a variable exists? - Stack Overflow

3.関数の戻り値を代入する際にタプルパック、シーケンスアンパックをつかう書き方

タプルパックというのは','で繋がれた複数の値を評価すると暗黙のうちにタプルとして評価されるというものです。

>>> hoge = 1, 2, 3
>>> hoge
(1, 2, 3)

シーケンスアンパックは','で繋がれた複数の変数に対してタプル・リスト・文字列などiterableな値を代入すると要素数が一致する場合自動的に展開されて代入されるというものです。

>>> a, b, c = (3, 5, 7)
>>> a
3
>>> b
5
>>> c
7
>>> d, e, f = 'ABC'
>>> d
'A'
>>> e
'B'
>>> f
'C'

両者の複合技として下のような代入の仕方が可能になっています。

>>> a, b, c = 1, 'A', 5
>>> a
1
>>> b
'A'
>>> c
5

これを関数の戻り値を代入する際に使うと複数の値を戻したい場合に簡潔に書くことができます。

def test():
    return 1, 3, 5

a, b, c = test()

print a  # 1
print b  # 3
print c  # 5

辞書やリストを戻しそこから個別の値を取り出して使用する方法もありますが、辞書やリストにするほど結び付きが強くない複数の値を戻す際に便利ではないでしょうか。

import math

def mean(*args):
    '引数の相加平均と相乗平均を求める'
    add = 0.0
    multi = 1.0
    L = len(args)
    for v in args:
        add += v
        multi *= v

    arith = add / L
    geo = math.pow(multi, 1.0 / L)
    
    return arith, geo

arith, geo = mean(5, 20)

print arith # 12.5
print geo   # 10.0

参考:タプル(3) - バリケンのPython日記 - pythonグループ

以上、私がPythonを勉強していて「こういう書き方があったのか」と思ったことでした。
実際はもっとたくさんあったと思うのですがすぐに思いつくのがこの3つだったので今回は3つにしました。

Google App Engineのリソース割り当てを使いきった場合の処理について

Google App Engine公式ページのドキュメントは日本語化されていない部分が多く、リソース割り当てを使い切った場合の処理についてもその一つです。
これは実行中のコード内からリソースの状態を知るおそらく唯一の方法であるにも関わらず日本語版がないので訳してみました。

原文はQuotas  |  App Engine Documentation  |  Google Cloudです。
原文のライセンスはCC BY 3.0です。サンプルコードはApache 2.0 Licenseです。


訳ここから

リソースが尽きた場合

アプリケーションが割り当てられたリソースを使いきった場合、そのリソースは割り当てが補充されるまで使用できなくなります。これは割り当て補充までアプリケーションが機能しなくなるかもしれないということです。

リクエストの処理を開始するために必要なリソースについては、それらが尽きていた場合、既定でApp Engineはリクエストハンドラを呼び出す代わりにHTTP 403 Forbiddenステータスコードを返します。以下のリソースがこのパターンに該当します。
・Bandwidth, incoming and outgoing

ヒント:アプリケーションが割り当てを使いきった場合に自作のエラーページを送出するように設定することもできます。詳細はCustom Error Responsesのドキュメントを御覧ください。Python Java

上記以外のすべてのリソースはそれらが尽きていた場合、アプリ内で利用しようとすると例外が発生します。この例外をアプリケーションでキャッチして扱うことでユーザーに対して親切なエラーメッセージを表示するといったことができます。Python APIではこの例外は"apiproxy_errors.OverQuotaError"です。Java APIではこの例外は"com.google.apphosting.api.ApiProxy.OverQuotaException"です。

以下のサンプルはe-mail関連の割り当てが上限を超えていた場合にSendMessage()メソッドによって発生するOverQuotaErrorをキャッチする方法を示しています。

try:
  mail.SendMessage(to='test@example.com',
                   from='admin@example.com',
                   subject='Test Email',
                   body='Testing')
except apiproxy_errors.OverQuotaError, message:
  # エラーをログ出力
  logging.error(message)
  # ユーザーに対して有益な情報を表示
  self.response.out.write('The email could not be sent. '
                          'Please try again later.')

もしアプリケーションが予想外にシステムリソースを使い切ったのでしたらアプリケーションの性能を分析してみることをお考えください。

あなたのアプリはデフォルトの割り当て上限を超えていますか?もしあなたがプレミアアカウントのお客様でしたらサポートに連絡し追加の割り当てを請求することができます。もしプレミアアカウントでなければMail API割り当ての拡大を申し込むか、それ以外の割り当て上限の拡大のために機能改善リクエストを送ることができます。もしくはGoogleのプレミアアカウントについてご確認いただくこともできます。

https://developers.google.com/appengine/docs/quotas#When_a_Resource_is_Depleted

訳ここまで

上記のサンプルコードでは書かれていませんが"apiproxy_errors"は"google.appengine.runtime"にあるモジュールなので

from google.appengine.runtime import apiproxy_errors

のようにインポートして使います。
原文のプレミアアカウントうんぬんの話は課金しての上限も使い切った場合の話だと思います。

GAEのmemcacheモジュールの関数がAptanaやEclipse+PyDevで未定義とされる原因と対策

私はGAEのコードを書く際Aptana studioを使用しているのですが、正しくPathが通っている状態でもmemcacheモジュールだけコード解析でエラーが出ます。
具体的にはmemcacheモジュール内の関数"memcache.get()"などを使用するとその関数は定義されていないというエラーが出ます。

もちろん実際にはこの関数は定義されており使用可能です。
Aptanaがエラーを出していても問題なく動作するので放っておいてもいいのですがエラーがでたまま作業をするのは目障りですし、本当のエラーがわかりづらくなってしまいます。そこでmemcacheモジュールだけがエラーとなる理由を調べてみました。

結論から書きますとこれらモジュール直下の関数が、モジュール初期化時に動的に定義されるものであることが原因です。Aptanaのコード解析は静的に定義されている関数のみをチェックしているので動的に定義されるものは未定義であると判断しているわけです。

memcacheには"memcache.get()"のような関数インターフェースとは別にClientインターフェースという物があり、これはClientオブジェクトを通してmemcacheを読み書きする仕組みです。

from google.appengine.api import memcache


class DataUpdater(webapp2.RequestHandler):
    def get(self):
        cache = memcache.Client()  # Clientオブジェクトを作成
        test_data = cache.get('sample_data')

        cache.set('test', test_data)

というようにClientオブジェクトを定義してそれを用いてmemcacheにアクセスします。この方法であればAptanaのコード解析もエラーが出ませんしサジェストも動作します。

上でmemcacheモジュールは初期化される際に"memcache.get()"などの関数インターフェースを動的に定義していると書きましたが、どのようにそれを行っているかというとモジュール内で上記のClientオブジェクトの作成を行っています。

このClientオブジェクトはmemcacheモジュール内で_CLIENTという名前で定義されこのオブジェクトの".get()"や".set()"などすべてのメソッドがmemcache直下の同名関数から参照されるように動的に定義されます。
つまり"memcache.get()"は実際には"memcache._CLIENT.get()"を呼び出していて"memcache.set()"は"memcache._CLIENT.set()"を呼び出しているわけです。

これを踏まえ、import方法を工夫してAptanaがエラーをださず、サジェストやコード補完も有効になるようにしてみます。

from google.appengine.api import memcache as _memcache
memcache = _memcache._CLIENT

class DataUpdater(webapp2.RequestHandler):
    def get(self):
        test_data = memcache.get('sample_data')

        memcache.set('test', test_data)

このようにmemcacheというシンボルがmemcache._CLIENTを参照するようにimport後に定義するとそれ以降のコードでは"memcache.get()"などが使用できます。開発中はこのように書いておきデプロイ時にimportを通常のように戻してもよいですし、気持ち悪くなければこのままデプロイしても問題ないと思います。

もっと手っ取り早い対策としてAptanaを「エラーを出さないように設定する」という方法もあります。
その場合は"Preferences>PyDev>Editor>Code Analysis"から"Undefined"のタブを選んで"Undefined variable from import"の項目を"Ignore"に設定します。

ただしこの方法はあくまでエラーを出さないように設定するだけなのでサジェストやコード補完などの機能は使用できません。また本当のエラーも警告されなくなってしまいます。

まとめ

memcacheモジュールの関数がエラーになる原因はモジュール初期化時に動的に定義されているから。
対策は

  • memcache.Client()でインスタンスを作成してそれを用いる
  • import方法に細工する
  • そもそもエラーが出ないようにAptanaを設定する

GAEデータストアのModelクラスのインスタンスからそのkey_nameを取得する方法について

これまでModel、Key、Propertyの関係を理解しないままデータストアを使用していた事を改めて思い知りました。(^_^;)

下のような都道府県の面積と人口を記録するデータストアのエンティティがあるとします。

class Prefecture(db.Model):
    area = db.FloatProperty()
    population = db.IntegerProperty()

pref1 = Prefecture(key_name='Tokyo',
                   area=2188.67,
                   population=13269061)
pref1.put()
pref2 = Prefecture(key_name='Chiba',
                   area=5156.61,
                   population=6192785)
pref2.put()

ここから人口が1000万人以上の都道府県を取得し、表示したい場合下のようなクエリを使うと思います。

query = Prefecture.all().filter('population >', 10000000).order('-population')

あとはここから都道府県名すなわちkey_nameを取得すればいいわけですが、この方法がわからずしばらく悩んでしまいました。

私はkey_nameをデータストア上でユニークであることが保証されている便利なプロパティの一種であるかのように使用していたので

for val in query:
    name = val.key_name

のようにkey_nameを取得しようとしてしまいました。これはAttributeErrorになります。
key_nameを取得するには次のように書く必要があります。

for val in query:
    name = val.key().name()

valはModelクラスのインスタンスでありval.key()が返すのはKeyクラスのインスタンスなのでkey_nameはval.key().name()で取得するということです。

要は「key_nameはプロパティの一種などではない」ということなのですがインスタンス作成時に下のようにプロパティと同列に引数で指定できるため思い違いをしていました。

pref1 = Prefecture(key_name='Tokyo',
                   area=2188.67,
                   population=13269061)

key_nameはそのエンティティを他のエンティティと区別するための識別子であるKeyオブジェクトの要素でありエンティティ自体のプロパティとは異なります。
このことはエンティティ、プロパティ、キー  |  Python の App Engine スタンダード環境  |  Google Cloudに書かれています。
このページ目を通したはずだったのですが難しいことが書いてあるな〜という感じで理解できていませんでした。
今回の問題に当って改めて読んでみてやっとModel、Key、Propertyの関係が分かりました。

WikipediaのAPIを使って情報を取得するクラスを書いてみました

先日公開したスコアアタックしりとりもまだ予定のすべての機能が完成したわけではないのですが、最近は次は何作ろうかなということを考えて様々なサービスのAPIを調べています。

今回WikipediaAPIに接続して情報を取得するクラスを書いてみました。
Wikipediaの、と書きましたがWikipediaに使われているソフトであるMediawikiを使用したサイトであればAPIは共通なのでWikipedia専用というわけではありません。

Wikipedia APIの仕様についてはMediaWiki API ヘルプ - WikipediaなどAPIに引数なしでアクセスすることで見ることができます。
このクラスではURLのパラメータに当たる部分を辞書で引数に渡しインスタンスを作成すると、すぐにAPIに接続し受け取ったレスポンスをDOM化します。DOMはインスタンスのdomというプロパティからアクセスします。
formatをXML以外にした場合はDOM化はされずrawdataプロパティがレスポンスをそのまま参照します。

# coding: utf-8

import urllib
import urllib2
from xml.dom.minidom import parse as parseXML

URL = 'http://ja.wikipedia.org/w/api.php?'
BASIC_PARAMETERS = {'action': 'query',
                    'format': 'xml'}


class WikiHandler(object):
    def __init__(self, parameters, titles=None, url=URL):
        self._url = url if url.endswith('?') else url + '?'

        self._parameters = {}
        self._parameters.update(BASIC_PARAMETERS)
        self._parameters.update(parameters)

        if titles:
            self._parameters['titles'] = titles

        self.rawdata = self._urlfetch(self._parameters)

        if self._parameters['format'] == 'xml':
            self.dom = parseXML(self.rawdata)
            print 'DOM ready.'

    def _urlfetch(self, parameters):
        parameters_list = []

        for key, val in parameters.items():
            if isinstance(val, basestring):
                val = val.encode('utf-8')
            else:
                val = str(val)

            val = urllib.quote(val)
            parameters_list.append('='.join([key, val]))

        url = self._url + '&'.join(parameters_list)

        print 'Accessing...\n', url

        return urllib2.urlopen(url, timeout=20)

日本語版Wikipediaから「プログラミング言語」というカテゴリに属している記事とサブカテゴリの一覧を取得して表示するコードは下のようになります。

def main():
    parameters = {'list': 'categorymembers',
                  'cmlimit': 500,
                  'cmtitle': u'Category:プログラミング言語'}

    page = WikiHandler(parameters)

    elelist = page.dom.getElementsByTagName('cm')

    print elelist.length # 要素数

    for ele in elelist:
        print ele.getAttribute('title')


if __name__ == '__main__':
    main()

結果は下のようになります。(長いので後半は省略)

Accessing...
http://ja.wikipedia.org/w/api.php?action=query&cmtitle=Category%3A%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E&cmlimit=500&list=categorymembers&format=xml
DOM ready.
278
プログラミング言語
Template:ProgLangCompare
プログラミング言語一覧
プログラミング言語の比較
プログラミング言語年表
4GL
A+
A-0 System
ABAP
ABC (プログラミング言語)
Ada
ALGOL
APL
Autocode
AutoIt
AWK
B言語
BASIC

以下省略

GAEでcronを設定する

cronは一般的には

crontab(クロンタブ、あるいはクローンタブ、クーロンタブとも)コマンドはUnix系OSにおいて、コマンドの定時実行のスケジュール管理を行うために用いられるコマンドである。

http://ja.wikipedia.org/wiki/Crontab

とのことですがGoogle App Engineのそれは一定間隔で自アプリの指定URLにHTTP-GETリクエストを送る機能です。
URLリクエストを受け取って行う動作は通常のhttpリクエストを受けた場合同様自由に記述できるので、実質的には任意の間隔で指定した動作をする機能として使用できます。

私は「スコアアタックしりとり」のなかでデータストアをメンテナンスするために使っています。
スコアアタックしりとりではユーザーからのアクセスがあるとセッションIDを発行し、単語が入力される度にサーバでプレイ中のデータとしてデータストアに保存しています。これらのデータはユーザーが「最初からプレイ」のボタンを押すと今回のゲームが終了したのだと判断し削除されますが、プレイの途中でブラウザを閉じた場合などはプレイ途中のデータがデータストアに残ってしまいます。
これをそのまま放置しているとプレイ途中のデータがデータストアにたまっていってしまうため、ゲームの制限時間を30分に設定しそれを過ぎたデータをcronをつかって削除しています。
制限時間は30分ですが実際には誤差を考慮して1時間経過で削除しています。この処理を1時間に1回行います。
これ以外にはてなキーワードに問い合わせた単語のデータも30日間の保存期間が過ぎたら削除しています。こちらは1日に1回行います。

cronを使うにはcron.yamlというファイルを作成し、app.yamlと同じ階層に置いておきます。
スコアアタックしりとりの場合は下のように書いています。

cron:
- description: daily WordsStock maintainer
  url: /maintain/wordsstock
  schedule: every 24 hours

- description: hourly UserProfile maintainer
  url: /maintain/userprofile
  schedule: every 1 hours

descriptionはDashboardに表示されるその処理の説明です。自由に書くことができます。
urlはcronがアクセスするurlです。
scheduleはcronを実行する間隔ということになります。これは書式が決まっています。

上のファイルですと24時間に1回'/maintain/wordsstock'に、1時間に1回'/maintain/userprofile'にcronがGETリクエストを送ることになります。

cronがリクエストを送るurlはブラウザからアクセスできる通常のurlと全く同じものなのでapp.yamlでもcron用に設定しておく必要があります。
スコアアタックしりとりの場合は下のように設定しています。

application: scoreattackshiritori
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /image
  static_dir: image

- url: /js
  static_dir: js

- url: /stylesheets
  static_dir: stylesheets
  
- url: /maintain/wordsstock
  script: maintain.app
  login: admin

- url: /maintain/userprofile
  script: maintain.app
  login: admin

- url: /.*
  script: main.app


libraries:
- name: jinja2
  version: latest

cronの作業内容を記述したファイルはゲーム用のファイルとは別にしてmaintain.pyとしているためscript指定はmaintain.appとなっています。
cron用のurlは通常のブラウザからでもアクセスできる普通のurlなので

  login: admin

を指定しておくことで予期せず第三者がこのurlにアクセスして処理が実行されることを防止できます。cronはAdministrator権限で実行されるためこの記述があっても動作できます。

maintain.pyの中身は下のようになっています。(WordsStockは単語に関する情報を保存するエンティティのクラス、UseProfileはプレイ中のデータを保存するそれです)

# coding: utf-8

import webapp2
import datetime
from google.appengine.ext import db
from main import WordsStock, UserProfile

# WORDS_LIFE_DAYSはWordsStockエンティティの寿命
# 単位は日
WORDS_LIFE_DAYS = 30
# USERPROFILE_LIFE_HOURSはUserProfileエンティティの寿命
# 単位は時間
USERPROFILE_LIFE_HOURS = 1


class MaintainDB(webapp2.RequestHandler):
    @staticmethod
    def _deleteDB(target):
        if target == WordsStock:
            lifetime = datetime.timedelta(days=WORDS_LIFE_DAYS)
            indexname = 'stockdate'
        elif target == UserProfile:
            lifetime = datetime.timedelta(hours=USERPROFILE_LIFE_HOURS)
            indexname = 'reg_date'
        else:
            return

        threshold = datetime.datetime.now() - lifetime

        results = target.all().filter(indexname + ' <', threshold)

        db.delete(results)


class MaintainWordsStock(MaintainDB):
    def get(self):
        self._deleteDB(WordsStock)


class MaintainUserProfile(MaintainDB):
    def get(self):
        self._deleteDB(UserProfile)

app = webapp2.WSGIApplication([('/maintain/wordsstock', MaintainWordsStock),
                               ('/maintain/userprofile', MaintainUserProfile)]
                              )

URLフェッチのタイムアウト時間を設定する

先日デプロイしたスコアアタックしりとりなのですがURLフェッチに意図しないエラーがあったようで原因を調べてみました。
ちなみにURLフェッチというのはGoogle App Engine上のアプリケーションが外部のサーバから必要な情報をhttp(s)で取得することです。

スコアアタックしりとりではプレイヤーが単語を入力するとJavascriptで簡単なバリデーション(すでに使った単語ではないか、10個以上の単語が入力されていないかなど)を行ったあとサーバに送られしりとりのルールに適合しているか確認されます。
この際しりとりですから各単語の「読み方」の情報が必要なので、これをはてなキーワードAPIを利用して取得します。この際URLフェッチを使うわけですがログをみると予期せず失敗するケースが発生していました。

はてなキーワードAPIに問い合わせた結果うまく情報が取得できなかった場合プレイヤーには「○○ははてなAPIの問題で選択出来ません。」というメッセージが表示されます。これは以前書いたはてなキーワードAPIに存在しているバグに該当する単語に備えたものなのですがそれ以外の単語でも発生していました。

調べたところ、はてなキーワードAPIへの問い合わせ時にタイムアウトしてしまうことが原因のようです。GAEのURLフェッチはデフォルトでは5秒でタイムアウトしてしまうので、はてなサーバが混雑しているなど返答が遅れるケースでこの問題が発生するようです。そこでURLフェッチのタイムアウト時間を延長することにしました。

これまではPython標準ライブラリであるurllib2のurlopenメソッドを使って

dom = parse(urllib2.urlopen(requestURL))

というようにリクエストを行なっていました。(以前作成していたローカルのはてなキーワードしりとりと全く同じです。)

このままtimeout引数を設定して

dom = parse(urllib2.urlopen(requestURL, timeout=20))

というようにしても良いのですが、今回GAEのライブラリに用意されているurlfetchを使うように変更しました。GAEライブラリのurlfetchはurllib2にくらべGAEの仕様に合わせた例外処理ができるなどのメリットがあります。

GAEの仕様上設定可能なタイムアウト時間は最大60秒です。公式のチュートリアルで最大は10秒と書かれていますがこれは古い仕様でチュートリアルの日本語化が追いついていないのだと思います。英語のチュートリアルによると現在の仕様はユーザーからのhttpリクエストを受けて行うurlfetchでは最大60秒、task queue および cron job から行うurlfetchでは最大10分です。

今回はタイムアウトのリミットを20秒に設定することにします。urlfetchは

from google.appengine.api import urlfetch

data = urlfetch.fetch(url=requestURL, deadline=20)

というように使います。タイムアウトまでの時間を決める引数名はtimeoutではなくdeadlineです。
urllib2.urlopenとは戻り値の型が違います。
urllib2.urlopenは受け取ったデータをファイル型オブジェクトとして返すので

import urllib2
from xml.dom.minidom import parse

dom = parse(urllib2.urlopen(requestURL, timeout=20))

という形でDOM化することができましたが、GAEのurlfetch.fetchはcontentというプロパティが本文にあたる部分の文字列を参照しています。その他のプロパティなどはこちら
そのためparse関数ではなくparseString関数を使います。

from google.appengine.api import urlfetch
from xml.dom.minidom import parseString

content = urlfetch.fetch(url=requestURL, deadline=20).content
dom = parseString(content)

スコアアタックしりとりも上のように変更しました。(実際は例外処理があります)

これでしばらく様子をみようと思います。

ぜひ一度プレイしてみてください
スコアアタックしりとり