ヒントとコツ¶
コードの重複の防止¶
可能な限り、同じコードを複数の場所にコピーして貼り付けるべきではありません。工夫が必要な場合もありますが、コードを1か所にまとめることで、コードのデザインの変更やバグの修正をしたりする必要があるときに、通常は多くの労力を節約できます。
以下は、コードの再利用を実現するためのいくつかの手法です。
アプリのコピーを複数作成しないでください¶
可能であれば、アプリのフォルダーをコピーしてわずかに異なるバージョンを作成することは避けてください。コードが重複しているため、保守が困難になります。
複数のラウンドを用意したいだけなら、 NUM_ROUNDS
を設定することで解決できますまた、わずかに異なるバージョン(たとえば、異なる処理)が必要な場合は、異なる 'treatment'
パラメーターを持つ2つのセッション構成を作成し、コード上で session.config['treatment']
についてチェックするなど、 処理の割り当て で説明されている手法を使用する必要があります。
多くのフィールドを作成する方法¶
アプリに、次のようなほぼ同じフィールドが多数あるとします。
class Player(BasePlayer):
f1 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f2 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f3 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f4 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f5 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f6 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f7 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f8 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f9 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
f10 = models.IntegerField(
choices=[-1, 0, 1], widget=widgets.RadioSelect,
blank=True, initial=0
)
# etc...
これは非常に複雑です。単純化する方法を探す必要があります。
フィールドはすべて別々のページに表示されていますか?そうであれば、1つのフィールドだけ用意し、10ラウンドのゲームに変換することを検討してください。
それが不可能な場合は、フィールドを返す関数を定義することで、繰り返されるコードの量を減らすことができます。
def make_field(label):
return models.IntegerField(
choices=[1,2,3,4,5],
label=label,
widget=widgets.RadioSelect,
)
class Player(BasePlayer):
q1 = make_field('I am quick to understand things.')
q2 = make_field('I use difficult words.')
q3 = make_field('I am full of ideas.')
q4 = make_field('I have excellent ideas.')
複数のラウンドを使用してページの重複を防止¶
ほぼ同じページが多数ある場合は、1ページだけを作成し、それを複数のラウンドでループさせることを検討してください。次のような場合、コードを簡略化できる可能性があります。
# [pages 1 through 7....]
class Decision8(Page):
form_model = 'player'
form_fields = ['decision8']
class Decision9(Page):
form_model = 'player'
form_fields = ['decision9']
# etc...
検証方法の重複を避ける¶
多くの繰り返される FIELD_error_message メソッドがある場合は、それらを単一の error_message 関数に置き換えることができます。例:
def quiz1_error_message(player, value):
if value != 42:
return 'Wrong answer'
def quiz2_error_message(player, value):
if value != 'Ottawa':
return 'Wrong answer'
def quiz3_error_message(player, value):
if value != 3.14:
return 'Wrong answer'
def quiz4_error_message(player, value):
if value != 'George Washington':
return 'Wrong answer'
代わりに、ページでこの関数を定義することもできます。
@staticmethod
def error_message(player, values):
solutions = dict(
quiz1=42,
quiz2='Ottawa',
quiz3='3.14',
quiz4='George Washington'
)
error_messages = dict()
for field_name in solutions:
if values[field_name] != solutions[field_name]:
error_messages[field_name] = 'Wrong answer'
return error_messages
(通常 error_message
は、単一のエラーメッセージを文字列として返すために使用されますが、辞書型を返すこともできます。)
ページ関数の重複を避ける¶
すべてのページ関数は、ページクラスから最上位の関数に移動できます。これは、同じ関数を複数のページで共有するための便利な方法です。たとえば、多くのページに次の2つの関数が必要だとします。:
class Page1(Page):
@staticmethod
def is_displayed(player: Player):
participant = player.participant
return participant.expiry - time.time() > 0
@staticmethod
def get_timeout_seconds(player):
participant = player.participant
import time
return participant.expiry - time.time()
これらの関数をすべてのページの前に移動させ( @staticmethod
を削除)、使用する必要があるどこからでも参照することができます。
def is_displayed1(player: Player):
participant = player.participant
return participant.expiry - time.time() > 0
def get_timeout_seconds1(player: Player):
participant = player.participant
import time
return participant.expiry - time.time()
class Page1(Page):
is_displayed = is_displayed1
get_timeout_seconds = get_timeout_seconds1
class Page2(Page):
is_displayed = is_displayed1
get_timeout_seconds = get_timeout_seconds1
(サンプルゲームで、 after_all_players_arrive
と live_method
は、このように定義されることが多いです。)
コードパフォーマンスの向上¶
get_players()
、 get_player_by_id()
、 in_*_rounds()
、 get_others_in_group()
のようなプレイヤーのリストやプレイヤーを返す関数の過剰な使用を避けるべきですこれらのメソッドはすべてデータベースクエリを必要としますが、処理に時間がかかる場合があります。
たとえば、このコードはデータベースにまったく同じプレイヤーを5回要求するため、冗長なクエリが発生してしまいます。
@staticmethod
def vars_for_template(player):
return dict(
a=player.in_round(1).a,
b=player.in_round(1).b,
c=player.in_round(1).c,
d=player.in_round(1).d,
e=player.in_round(1).e
)
これは次のように簡略化する必要があります。
@staticmethod
def vars_for_template(player):
round_1_player = player.in_round(1)
return dict(
a=round_1_player.a,
b=round_1_player.b,
c=round_1_player.c,
d=round_1_player.d,
e=round_1_player.e
)
さらに、コードをより読みやすくすることも期待できます。
可能であれば、StringFieldの代わりにBooleanFieldを使用してください¶
特に、2つの異なる値しかない場合は、多くの StringFields
は BooleanFields
に分解されるべきです。
treatment
という次のフィールドがあるとします。
treatment = models.StringField()
そして、 treatment
は4つの異なる値しか持っていないとします。
high_income_high_tax
high_income_low_tax
low_income_high_tax
low_income_low_tax
そのままの場合、ページでは次のように記述しなければなりません。
class HighIncome(Page):
@staticmethod
def is_displayed(player):
return player.treatment == 'high_income_high_tax' or player.treatment == 'high_income_low_tax'
class HighTax(Page):
@staticmethod
def is_displayed(player):
return player.treatment == 'high_income_high_tax' or player.treatment == 'low_income_high_tax'
そして、フィールドを2つの別々のBooleanFieldsに分割することを推奨します:
high_income = models.BooleanField()
high_tax = models.BooleanField()
最終的に、ページを次のように簡略化できます。
class HighIncome(Page):
@staticmethod
def is_displayed(player):
return player.high_income
class HighTax(Page):
@staticmethod
def is_displayed(player):
return player.high_tax
field_maybe_none¶
If you access a Player/Group/Subsession field whose value is None
, oTree will raise a TypeError
.
This is designed to catch situations where a user forgot to assign a value to that field,
or forgot to include it in form_fields
.
However, sometimes you need to intentionally access a field whose value may be None
.
To do this, use field_maybe_none
, which will suppress the error:
# instead of player.abc, do:
abc = player.field_maybe_none('abc')
# also works on group and subsession
注釈
field_maybe_none
is new in oTree 5.4 (August 2021).
An alternative solution is to assign an initial value to the field so that its value is never None
:
abc = models.BooleanField(initial=False)
xyz = models.StringField(initial='')