先日、昔使ったことがあるpytumblrを使う機会があったのですが、
久々に使うってこともあって、cloneしてコードを読んでいました。
TumblrはまだOAuth1.0ってのもあってか、昔と全然変わってなかった(多分)。
このクライアントは小さいコードなんですけど、自分は初めて読んだ時はすごく輝いて見えたのを覚えています。
OAuth1.0でAPI叩いてどうこうするようなラッパーはこういう感じで書くのか、
とアホなりに何回も読んでました。マジ読みやすいです。
そういえば「デコレーター便利じゃね?」って思ったのは、
以前もpytumblrを使った時に「これなんでこうなるんだ?」って思ってからでした。
アプリケーション名, アプリケーションウェブサイト, アプリケーションの説明,
デフォルトのコールバックURL
を入力して、スパムじゃないよー審査して「登録」
説明は入力しなくても平気かも。
あとコールバックURLも適当でいい。
そうするとアプリケーションが登録できるので、Explore APIのリンク押す。
すると各言語ごとのOAuth認証を親切に教えてくれる。楽です。
def validate_blogname(fn): """ Decorator to validate the blogname and let you pass in a blogname like: client.blog_info('codingjester') or client.blog_info('codingjester.tumblr.com') or client.blog_info('blog.johnbunting.me') and query all the same blog. """ @wraps(fn) def add_dot_tumblr(*args, **kwargs): if (len(args) > 1 and ("." not in args[1])): args = list(args) args[1] += ".tumblr.com" return fn(*args, **kwargs) return add_dot_tumblr
class TumblrRestClient(object): 略... @validate_blogname def avatar(self, blogname, size=64): """ Retrieves the url of the blog's avatar :param blogname: a string, the blog you want the avatar for :returns: A dict created from the JSON response """ print 'blogname', blogname url = "/v2/blog/{0}/avatar/{1}".format(blogname, size) return self.send_api_request("get", url) 略...
>>> from pytumblr import TumblrRestClient >>> client = TumblrRestClient( ... '<your consumer key>', ... '<your consumer secret>', ... '<your oauth token>', ... '<your oauth token secret>' ... ) >>> client.avatar("nabetama") {u'avatar_url': u'https://33.media.tumblr.com/avatar_47f135d0439f_64.png'}
このケースだと、client.avatarには"nabetama"って文字列しか渡してないのに、
tumblrのAPIを叩いて、avatar画像を取得しています。
TumblrRestClient#avatarだけを見ると、urlの加工をしているようには見えません。
TumblrRestClient#avatar 抜粋... url = "/v2/blog/{0}/avatar/{1}".format(blogname, size) return self.send_api_request("get", url)
helpers.validate_blognameデコレーターは関数を受け取って、
add_dot_tumblrを返します。
というわけで、add_dot_tumblrを見てみると、
*args, **kwargsを引数に取ります。このケースだと
client.avatar("nabetama")
class TumblrRestClient(object): ... def avatar(self, blogname, size=64) ...
”nabetama”
が渡されてくることになります。
if (len(args) > 1 and ("." not in args[1])):
引数があり, args[1]つまり、”nabetama”に”.”が入ってるかどうか
チェックします。”.”が入ってない場合、APIを叩くための文字列としては
不適切だということで、以下の処理に進みます。
しているのは*argsは仕様上tupleであり、tupleが不変の
シーケンス型(要素の変更が出来ない)なので、listオブジェクトに変更してるだけです。
“nabetama”は”nabetama.tumblr.com”へと変更されます。
return fn(*args, **kwargs)
デコレーターによってTumblrRestClient#avatarのインターフェースと
内部実装を変更するということになります(よね?)。
デコレーターがやってることも含めてavatarに詰め込むとこんな感じになります (例外は考慮しない)。
def avatar_(self, *args, **kwargs): if (len(args) and ("." not in args[0])): args = list(args) args[0] += ".tumblr.com" url = "/v2/blog/{0}/avatar/{1}".format(args[0], kwargs.get('size', 64)) return self.send_api_request("get", url)
呼び出す方はavatarの振る舞いの変更によって何も買えなくていいし、
何かを考慮する必要が一切ない!すごい!
前提として、functools.wrapsを使ってるんだけど、これを使わないと元の関数
(この場合はTumblrRestClient#avatar)のドキュメントや関数名が失われてしまう。
なので、デコレータを作るときにはfunctools.wrapsを使うのがお約束らしい。
>>> client.avatar <bound method TumblrRestClient.avatar of <pytumblr.TumblrRestClient object at 0x10dc66ad0>> >>> print client.avatar.__doc__ Retrieves the url of the blog's avatar :param blogname: a string, the blog you want the avatar for :returns: A dict created from the JSON response
なんということでしょう!
メソッド名とドキュメント文字列が…!!
>>> client.avatar <bound method TumblrRestClient.add_dot_tumblr of <pytumblr.TumblrRestClient object at 0x10c858d90>> >>> print client.avatar.__doc__
ということで、とりあえずfunctools.wrapsは使ったほうが色々と捗ります.