ヒントとコツ

コードの重複の防止

可能な限り、同じコードを複数の場所にコピーして貼り付けるべきではありません。工夫が必要な場合もありますが、コードを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_arrivelive_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つの異なる値しかない場合は、多くの StringFieldsBooleanFields に分解されるべきです。

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='')