【大規模スマホゲー】Python未経験エンジニアとの最初の1ヶ月OJTメモ

  • 471
    Like
  • 3
    Comment
More than 1 year has passed since last update.

スマホゲームの運用開発に参画したPython未経験エンジニア(スクリプト言語は経験有)にOJTで最初の1ヶ月に教えたことメモ。内容はPython環境構築、大規模スマホゲームを支える仕組み、Webアプリケーション開発、Pythonのつまずきどころです。

1. 目標

最初の1ヶ月で次の業務がこなせるようになるといいなと目標を立ててOJTしました。

  • 担当ゲームの特定マップをクリアして中級者になる(業務時間中にゲームしてレベル上げてもらう)
  • サポート付きでバグチケットに対応して修正プルリクを出せるようになる

2. Macにインストールしてもらったアプリ

スクリーンショット 2015-12-01 18.10.56.png

アプリ名 利用目的
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と環境構築
# 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するよう設定します。

.bash_profileに追加設定
vi ~/.bash_profile

# virtualenv
export WORKON_HOME=$HOME/.virtualenvs
source `which virtualenvwrapper.sh`
workon {{project_name}}

# workonを解除
# deactivate

bpython

Pythonの拡張された対話Shellです。適切に情報が補完されるため効率よく作業できます。

install
pip install bpython

スクリーンショット 2015-12-01 16.52.15.png

PyCharm

弊社ではPython未経験エンジニアは、エディタにPyCharmの利用を推奨しています。PEP8違反を検出して指摘するため正しいコードに繋がる点と、コードジャンプ機能が優秀で外部モジュールのソースコードが簡単に読め学習効率が高い点です。慣れたらVimでもEmacsでも好きにすればいいよといって導入してもらっています。

PyCharm Download

https://www.jetbrains.com/pycharm/

PyCharm稼働のための最低限の設定

  1. Project Interpreter と Project Structure
    Project: Hoge >>> Project Interpreter で virtualenvで指定した環境を指定する
    Project: Hoge >>> Project Structure で submoduleを指定する
    この対応でソースコードジャンプ機能が利用出来るようになります。ならないときは Project Structure でディレクトリを追加していきます。

  2. Djangoサポート有効化(Django使うPJのみ)
    Djangoで検索してEnable Django Supportを有効にして、project rootSettings を設定します。

  3. Show line numbersで検索して有効

  4. 空白スペース表示
    Show whitespacesで検索して有効化

  5. Pythonテンプレートの初期設定(任意)
    新規にPythonファイル生成時の初期コードを設定します。Python2.7なので次の値を設定します。
    Code Templatesで検索して、Python Scripts を選択して

PythonScripts
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

4. 大規模スマホゲームを支える仕組み

複雑で全部覚えるのは無理です。業務してたらそのうち覚えるから判らなくなったら質問してね前置きして、何回かに分けざっくり説明しました。

ステートレスなAPI設計が必要である理由

本番環境ではロードバランサー配下に複数台のアプリケーションサーバが存在します。ユーザからのrequestはロードバランサーで複数のサーバに振り分けられるため通信毎に独立したステートレスな設計が必要です。通信毎に独立した設計になっていないと通信1回目と2回目で同じサーバが応答するローカル環境では正しく動きますが、通信1回目と2回目で異なるサーバが応答する本番環境では意図しない動作を引き起こしてしまいます。

スクリーンショット 2015-12-01 18.47.57.png

アプリケーションサーバ台数は時間帯で変化する(AutoScaling)

サーバ維持費を節約するために、時間帯によってアプリケーションサーバ台数が変化することを次の資料を使って説明しました。開発者はサーバがいつ増減しても正常に動作するよう通信単位で処理を切り分けたコード(ステートレスな設計)を意識してコードを書く必要があります。

データベースの垂直分割と水平分割

データベース負荷を軽減するために、垂直分割と水平分割を行っています。次の資料を使ってざっくり説明しました。

スクリーンショット 2015-12-01 19.14.49.png

Content Delivery Network

CDNと略称で呼ばれることが多いです。世界中に張り巡らされたサーバにファイルをキャッシュしておき、エンドユーザに最も近いサーバからデータを転送することで、ダウンロード速度を高速化してユーザが快適にゲームプレイできるようにしています。ゲーム内の音楽や画像といったデータはCDN経由で配布しています。Akamaiがトップシェア。キャッシュサーバが世界中に存在する仕組み上キャッシュ削除に20分-40分掛かるため、データを上書きしない運用を行っています。

5. Webアプリケーション開発

チームでWebアプリケーションを開発するために

git flow

Git のブランチ管理はgit flow で行っています。経験者だったので説明不要でしたが知らない人に説明するときはgit flowの図で軽く説明してから、OJTで一緒にブランチを切って覚えてもらうことが多いです。

  1. 機能追加作業をする
    developブランチからfeature/XXXXXブランチを切って作業します。

  2. 本番のバグを修正する
    masterブランチからhotfix/XXXXブランチを切って作業します。デバッグが完了したらmasterにマージして本番反映します。

スクリーンショット 2015-12-01 19.51.10.png
Introducing GitFlowより

pull request

バグを防ぐためと教育のために、修正や機能追加を本番サーバに出す前にコードレビューを行っています。コードレビューをチームで取り組むためにStash(GitHub)のプルリクエスト機能を利用しています。

git submodule

スマホゲームで共通する機能は共通ライブラリチームから提供されています。ソースをコピーして取り込むとライブラリの更新が大変です。そのためgit のsubmodule 機能を利用して、ゲーム本体のリポジトリに外部の共通ライブラリのリポジトリをディレクトリとして取り込んでいます。

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でrange関数を試す
# 対話型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でrandint関数を試す
# 対話型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

  1. # -*- coding: utf-8 -*-
    コード内でマルチバイト文字(日本語)を利用するために宣言しています。

  2. __future__ モジュール
    Python2系にてPython3系の機能を利用するためのモジュールです。

  3. absolute_import
    Python2系と3系ではインポート順序が異なります。インポート順序を3系で統一するために宣言しています。absolute_import を利用すると2系でも標準モジュールとカレントモジュールが存在する場合、3系と同じように標準モジュールを優先してインポートされるようになります。

  4. 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型のリスト内包表記をサンプルとして記述しました。まずはどれがリスト内包表記か判別できるようになり、次にリスト内包表記を書けるよう練習していきましょう。

List型のリスト内包表記
# 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]
dict型のリスト内包表記
# 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 と名付けることが多いです。

Mixinクラスは単体動作を意図していない
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'
Mixinの利用例
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構文で実装すると、BeginRollbackCommit といった処理を構文内に隠蔽できます。

with構文ファイル書き込み
with open("text.txt", 'w') as text:
    text.write("Hello, world!")
with構文を定義してtext.close()を隠蔽した例
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 を判別するための情報を付与しています。

Djangoでplatformデコレータを実装する例

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を定義する

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を実装しています。

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 を多用するとコードの可読性が著しく低下するため、ここぞというタイミングでのみ利用しましょう。

settingsに特定パラメータが存在するか確認する
>>> from django.conf import settings
>>> print hasattr(settings.AAA)
False
setattr/getattrの利用例
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