スマホゲームの運用開発に参画したPython未経験エンジニア(スクリプト言語は経験有)にOJTで最初の1ヶ月に教えたことメモ。内容はPython環境構築、大規模スマホゲームを支える仕組み、Webアプリケーション開発、Pythonのつまずきどころです。
1. 目標
最初の1ヶ月で次の業務がこなせるようになるといいなと目標を立ててOJTしました。
- 担当ゲームの特定マップをクリアして中級者になる(業務時間中にゲームしてレベル上げてもらう)
- サポート付きでバグチケットに対応して修正プルリクを出せるようになる
2. Macにインストールしてもらったアプリ
アプリ名 | 利用目的 |
---|---|
Cyberduck | S3とSFTPでファイル転送とディレクトリ確認 |
Sequel Pro | データベース接続 |
rdm | Redis接続 |
3. Python環境構築
Python の環境構築方法は複数存在しますが、便利なvirtualenv
+ PyCharm
+ bpython
で構築する手順に沿って作業してもらいました。
virtualenv + virtualenvwrapper
virtualenv
を導入すると、複数のPython環境を簡単に構築・切り換えできるようになります。たとえばコマンド1つで Python2.7 + Django1.5
環境と Python3.5 + Django1.7
環境をworkon コマンド1つで切り替えられます。
# install
sudo easy_install pip
sudo easy_install virtualenv
sudo easy_install virtualenvwrapper
pip install pbr
sudo easy_install virtualenvwrapper
# virtualenv 環境構築
export WORKON_HOME=$HOME/.virtualenvs
source `which virtualenvwrapper.sh`
mkvirtualenv --no-site-packages --python=/opt/local/bin/python2.7 {{project_name}}
続いて最初はworkonコマンドを忘れてしまうため .bash_profile
にworkonするよう設定します。
vi ~/.bash_profile
# virtualenv
export WORKON_HOME=$HOME/.virtualenvs
source `which virtualenvwrapper.sh`
workon {{project_name}}
# workonを解除
# deactivate
bpython
Pythonの拡張された対話Shellです。適切に情報が補完されるため効率よく作業できます。
pip install bpython
PyCharm
弊社ではPython未経験エンジニアは、エディタにPyCharmの利用を推奨しています。PEP8違反を検出して指摘するため正しいコードに繋がる点と、コードジャンプ機能が優秀で外部モジュールのソースコードが簡単に読め学習効率が高い点です。慣れたらVimでもEmacsでも好きにすればいいよといって導入してもらっています。
PyCharm Download
PyCharm稼働のための最低限の設定
-
Project Interpreter と Project Structure
Project: Hoge >>> Project Interpreter
で virtualenvで指定した環境を指定する
Project: Hoge >>> Project Structure
で submoduleを指定する
この対応でソースコードジャンプ機能が利用出来るようになります。ならないときはProject Structure
でディレクトリを追加していきます。 -
Djangoサポート有効化(Django使うPJのみ)
Django
で検索してEnable Django Support
を有効にして、project root
とSettings
を設定します。
Show line numbers
で検索して有効
-
空白スペース表示
Show whitespaces
で検索して有効化 -
Pythonテンプレートの初期設定(任意)
新規にPythonファイル生成時の初期コードを設定します。Python2.7なので次の値を設定します。
Code Templates
で検索して、Python Scripts
を選択して
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
4. 大規模スマホゲームを支える仕組み
複雑で全部覚えるのは無理です。業務してたらそのうち覚えるから判らなくなったら質問してね前置きして、何回かに分けざっくり説明しました。
ステートレスなAPI設計が必要である理由
本番環境ではロードバランサー配下に複数台のアプリケーションサーバが存在します。ユーザからのrequestはロードバランサーで複数のサーバに振り分けられるため通信毎に独立したステートレスな設計が必要です。通信毎に独立した設計になっていないと通信1回目と2回目で同じサーバが応答するローカル環境では正しく動きますが、通信1回目と2回目で異なるサーバが応答する本番環境では意図しない動作を引き起こしてしまいます。
アプリケーションサーバ台数は時間帯で変化する(AutoScaling)
サーバ維持費を節約するために、時間帯によってアプリケーションサーバ台数が変化することを次の資料を使って説明しました。開発者はサーバがいつ増減しても正常に動作するよう通信単位で処理を切り分けたコード(ステートレスな設計)を意識してコードを書く必要があります。
データベースの垂直分割と水平分割
データベース負荷を軽減するために、垂直分割と水平分割を行っています。次の資料を使ってざっくり説明しました。
Content Delivery Network
CDNと略称で呼ばれることが多いです。世界中に張り巡らされたサーバにファイルをキャッシュしておき、エンドユーザに最も近いサーバからデータを転送することで、ダウンロード速度を高速化してユーザが快適にゲームプレイできるようにしています。ゲーム内の音楽や画像といったデータはCDN経由で配布しています。Akamaiがトップシェア。キャッシュサーバが世界中に存在する仕組み上キャッシュ削除に20分-40分掛かるため、データを上書きしない運用を行っています。
5. Webアプリケーション開発
チームでWebアプリケーションを開発するために
git flow
Git のブランチ管理はgit flow で行っています。経験者だったので説明不要でしたが知らない人に説明するときはgit flowの図で軽く説明してから、OJTで一緒にブランチを切って覚えてもらうことが多いです。
-
機能追加作業をする
developブランチからfeature/XXXXXブランチを切って作業します。 -
本番のバグを修正する
masterブランチからhotfix/XXXXブランチを切って作業します。デバッグが完了したらmasterにマージして本番反映します。
pull request
バグを防ぐためと教育のために、修正や機能追加を本番サーバに出す前にコードレビューを行っています。コードレビューをチームで取り組むためにStash(GitHub)のプルリクエスト機能を利用しています。
git submodule
スマホゲームで共通する機能は共通ライブラリチームから提供されています。ソースをコピーして取り込むとライブラリの更新が大変です。そのためgit のsubmodule 機能を利用して、ゲーム本体のリポジトリに外部の共通ライブラリのリポジトリをディレクトリとして取り込んでいます。
git submodule sync
git submodule update -i
PEP8 コーディング規約を守ろう
Pythonでは読みやすいコードは善であると言われています。PEP8で規定されているコーディング規約に沿っているかの観点でプルリクを確認し、違反していたら修正してもらいました。
6. Pythonのつまずきどころ
Python2.7系のお話です。ペアプロやペアコードリーディングしていて解説したところをピックアップ。Python特有の書き方は読んで理解できるようになるまで時間が掛かります。
対話型shellをつかいこなす
コードの書き方に自信がないときは対話型shellでまず試してみる癖をつけるとバグが減ります。たとえばrange(1, 100)
を呼び出したとき、末尾が99になるのか100になるか判らなくなったとき、対話型shellを起動すれば簡単に確認出来ます。
# 対話型shell起動
>>> bpython
# shell内で実行
>>> range(100)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 2
6, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 5
0, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 7
4, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 9
8, 99]
# 対話型shell内でimport可能
>>> import random
>>> random.randint(1, 2)
1
>>> random.randint(1, 2)
1
>>> random.randint(1, 2)
2
pyファイル最初の2行の呪文の意味
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
-
# -*- coding: utf-8 -*-
コード内でマルチバイト文字(日本語)を利用するために宣言しています。 -
__future__
モジュール
Python2系にてPython3系の機能を利用するためのモジュールです。 -
absolute_import
Python2系と3系ではインポート順序が異なります。インポート順序を3系で統一するために宣言しています。absolute_import
を利用すると2系でも標準モジュールとカレントモジュールが存在する場合、3系と同じように標準モジュールを優先してインポートされるようになります。 -
unicode_literals
Python3系では文字列をすべてユニコードとして扱うため、日本語の扱いが大変楽になっています。2系でも同様に文字列をユニコードで扱うために宣言しています。
文字列フォーマット %
と format
文字列フォーマットです。旧方式では %
を新方式では format
を利用します。 format
の利用が推奨されています。
>>> from __future__ import unicode_literals
>>> prefix = 'Player'
>>> player_id = 'A00001'
>>> _base = '%s:{player_id}'
>>> # %の利用
>>> print _base % prefix
Player:{player_id}
>>>
>>> key = _base % prefix
>>> # formatの利用
>>> print key.format(player_id=player_id)
Player:A00001
リスト内包表記
リスト内包表記を使うメリットは、コードを短く記述できる点と実行速度とメモリ消費が優れている点です。
Pythonのリスト内包表記は普段コードを記述する順番と異なる順番で記述するため、なかなか理解できず混乱します。代表的なList型とdict型のリスト内包表記をサンプルとして記述しました。まずはどれがリスト内包表記か判別できるようになり、次にリスト内包表記を書けるよう練習していきましょう。
# 1-30の範囲の偶数リストを生成する
even_list = []
for i in xrange(1, 31):
if i % 2 == 0:
even_list.append(i)
print even_list
# 1-30の範囲の偶数リストをリスト内包表記で生成する(普段と表記順が異なることを意識しよう)
print [i for i in xrange(1, 31) if i % 2 == 0]
# 実行結果
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
# 1-10の範囲でkeyの3乗を結果にもつdictを生成する
cubed = {}
for i in xrange(1, 11):
cubed[i] = i ** 3
print cubed
# 1-10の範囲でkeyの3乗を結果にもつdictをリスト内包表記で生成する
print {i: i**3 for i in xrange(1, 11)}
# 実行結果
{1: 1, 2: 8, 3: 27, 4: 64, 5: 125, 6: 216, 7: 343, 8: 512, 9: 729, 10: 1000}
{1: 1, 2: 8, 3: 27, 4: 64, 5: 125, 6: 216, 7: 343, 8: 512, 9: 729, 10: 1000}
Mixinクラス
Mixin とはオブジェクト指向プログラミング言語において、サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスです。Djangoではクラス名の最後にMixinと付けているためそれに習ってクラス名をHogehogeMixin
と名付けることが多いです。
class PlayerRunMixin(object):
def run(self):
print "StartRun:{player_name}".format(player_name=self.name)
return
>>> player = PlayerRunMixin()
>>> player.run()
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 3, in run
AttributeError: 'PlayerRunMixin' object has no attribute 'name'
class PlayerRunMixin(object):
def run(self):
print "StartRun:{player_name}".format(player_name=self.name)
return
class Player(PlayerRunMixin):
def __init__(self, _name):
self._name=_name
@property
def name(self):
return self._name
>>> player = Player('HamEgg')
>>> player.run()
StartRun:HamEgg
with構文
Pythonのwith構文は、可読性の向上と処理の後に特定プログラムを実行する用途で利用されます。たとえばトランザクション処理をwith構文で実装すると、Begin
、Rollback
、Commit
といった処理を構文内に隠蔽できます。
with open("text.txt", 'w') as text:
text.write("Hello, world!")
class FileSaved(object):
def __init__(self, path):
self.text = open(path, 'w')
def __enter__(self):
return self.text
def __exit__(self, type, value, traceback):
self.text.close()
with FileSaved("text2.txt") as text:
text.write("Hello, world!")
デコレータ
デコレータを利用すると関数をラップすることが出来ます。たとえばこちらのデコレータサンプル実装は、ViewのHTTP GET
関数を拡張してHTTP Header を読みこんで端末がiOSかどうかを判定するself.is_ios
を追加しています。HTTP Header はHTTP 通信を拡張した俺俺フォーマットでGooglePlayStore かAppStore を判別するための情報を付与しています。
class IndexView(ApiView):
@platform
def get(self, request, params, *args, **kwargs):
print self.is_ios, self.is_android
def platform(method):
@wraps(method)
def _wrapped_method(self, request, *args, **kwargs):
_platform = request.META.get('HTTP_X_DEVICE_OREORE_PLATFORM')
self.is_ios = False
self.is_android = False
if _platform == 'ios':
self.is_ios = True
elif _platform == 'android':
self.is_android = True
return method(self, request, *args, **kwargs)
return _wrapped_method
■ デコレータ資料
Pythonのデコレータを理解するための12Step
propertyのsetterを定義する
class Player(object):
def __init__(self, _name):
self.name=_name
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name=name
>>>
>>>
>>> p = Player('佐藤')
>>> print p.name
佐藤
>>> p.name = '田中'
>>> print p.name
田中
super
superを利用すると、多重継承時にコンストラクタといったクラス全体にまたがるメソッドチェーンを簡単に上書きできます。Python の super のメリットとデメリットが解説記事として優れています。
サンプルではDjangoでiOSだけHTTP-Status403エラーを応答するViewを実装しています。
class IndexView(ApiView):
@platform
def get(self, request, params, *args, **kwargs):
print self.is_ios, self.is_android
...
class CustomView(IndexView):
def get(self, request, params, *args, **kwargs):
if self.is_ios:
return HttpResponse(status=403)
return super(CustomView, self).get(self, request, params, *args, **kwargs)
hasattr / setattr / getattr
hasattrを使ったsettingsに特定値が存在するか確認する方法と、setattr / getattr を利用したゲーム内ユニットのパラメータ設定例を示します。setattr / getattr を多用するとコードの可読性が著しく低下するため、ここぞというタイミングでのみ利用しましょう。
>>> from django.conf import settings
>>> print hasattr(settings.AAA)
False
class Enemy(object):
# 初期値
_str_initial = 0
_vit_initial = 0
_int_initial = 0
_mind_initial = 0
# レベルアップ補正
_str_level = 0
_vit_level = 0
_int_level = 0
_mind_level = 0
@property
def str(self):
return self._str_initial + self._str_level
@property
def vit(self):
return self._vit_initial + self._vit_level
@property
def int(self):
return self._int_initial + self._int_level
@property
def mind(self):
return self._mind_initial + self._mind_level
>>> status = ['str', 'vit', 'int', 'mind']
>>> enemy = Enemy()
>>>
>>> # ランダムパラメータ設定
>>> import random
>>> for _s in status:
... setattr(enemy, '_{}_initial'.format(_s), random.randint(10, 20))
... setattr(enemy, '_{}_level'.format(_s), random.randint(1, 10))
...
>>> # パラメータ出力
>>> print enemy.str
20
>>> print enemy._str_initial, enemy._str_level
15 5
>>> for _s in status:
... print 'total-{}:{}'.format(_s, getattr(enemy, _s))
...
total-str:20
total-vit:23
total-int:18
total-mind:17