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です。
訳ここから
https://developers.google.com/appengine/docs/quotas#When_a_Resource_is_Depletedリソースが尽きた場合
アプリケーションが割り当てられたリソースを使いきった場合、そのリソースは割り当てが補充されるまで使用できなくなります。これは割り当て補充までアプリケーションが機能しなくなるかもしれないということです。
リクエストの処理を開始するために必要なリソースについては、それらが尽きていた場合、既定で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のプレミアアカウントについてご確認いただくこともできます。
訳ここまで
上記のサンプルコードでは書かれていませんが"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"に設定します。
ただしこの方法はあくまでエラーを出さないように設定するだけなのでサジェストやコード補完などの機能は使用できません。また本当のエラーも警告されなくなってしまいます。
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を調べています。
今回WikipediaのAPIに接続して情報を取得するクラスを書いてみました。
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)
スコアアタックしりとりも上のように変更しました。(実際は例外処理があります)
これでしばらく様子をみようと思います。
ぜひ一度プレイしてみてください
スコアアタックしりとり