提示与技巧

避免代码重复

尽可能避免复制粘贴同一段代码到多个地方是很好的习惯。尽管有时需要一些思考才能明白如何避免复制粘贴代码,但是将代码只重复一次通常会在你需要更改设计或者修复错误时为你节省很多时间和精力。

下面是一些实现代码复用的技巧。

不要将你的应用复制多份

你应当尽可能避免复制你的应用文件夹来创建一个有细微差别的版本,因为此时重复代码很难维护。

如果你需要多轮游戏,可通过设置 NUM_ROUNDS 来实现。如果你需要略微不同的版本(例如不同的实验组),那么你应当使用 实验组 中所描述的技巧,比如创建两个session config在其中设置不同的 'treatment' 参数,并在你的代码中检查 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...

这太复杂了,你应当找到一个能将其简化的方法。

这些字段都显示在单独的页面上吗?如果是的,考虑将其转换为一个仅有一个字段的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.')

通过使用多轮游戏避免代码重复

如果你有很多几乎完全一样的页面,考虑仅保留一个页面并将游戏重复多轮。你的代码可被简化的一个标志是它看上去像下面这样:

# [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
    )

作为额外的好处,这通常使得代码更具有可读性。

使用BooleanField而不是StringField,如果可能的话

许多 StringFields 应当被转化为 BooleanFields,尤其是其仅有2个不同的值时。

假定你有一个字段叫做 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'

将其化为两个单独的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='')