
oTree 新写法(New Format)一文看懂:从老格式到单文件、静态方法的过渡
我前阵子把一个老项目从 oTree 旧格式迁到“新写法”,一路踩坑,也把官方文档 https://www.otree.org/newformat.html 翻了个底朝天。下面这篇就按“能跑起来、能维护、能迁移”的思路,把新写法讲清楚。看完你就能用最新写法开工,或者把旧项目平稳升级。
一、到底什么是“新写法”
- 单文件:一个 app 用一个 Python 文件(通常是 app 目录下的
__init__.py
)就能写完,不再分models.py / pages.py
。 - Page/WaitPage 方法改成
@staticmethod
,不再用self
;方法第一个参数直接拿到player/group/subsession
。 - 常量类从
Constants
简写为C
。 - 货币使用
cu()
(currency utility),例如cu(100)
。 - 会话钩子与管理报表用模块级函数,例如
creating_session(subsession)
、vars_for_admin_report(subsession)
。
这套新写法从 oTree 5 开始普及,官方推荐。写起来更短,减少“在类里套方法”的心智负担。
二、最小可运行示例
假设你的 app 叫 survey
,目录结构大概是:
survey/__init__.py
survey/templates/survey/Intro.html
survey/templates/survey/Results.html
__init__.py
示例:
from otree.api import *
doc = """
一个最小示例:收年龄,展示结果
"""
class C(BaseConstants):
NAME_IN_URL = 'survey'
PLAYERS_PER_GROUP = None
NUM_ROUNDS = 1
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
pass
class Player(BasePlayer):
age = models.IntegerField(min=18, max=99, label='你的年龄')
class Intro(Page):
form_model = 'player'
form_fields = ['age']
class Results(Page):
@staticmethod
def vars_for_template(player: Player):
return dict(age=player.age)
page_sequence = [Intro, Results]
对应模板 Intro.html
:
{% block title %}填写信息{% endblock %}
{% block content %}
{{ formfields }}
{{ next_button }}
{% endblock %}
对应模板 Results.html
:
{% block title %}结果{% endblock %}
{% block content %}
你的年龄是:{{ age }}
{{ next_button }}
{% endblock %}
三、文件结构与命名规则(和旧版有什么不同)
- 单文件:一个 app 只需要
__init__.py
;以前的models.py
、pages.py
合并到一起。 - 常量类:
Constants → C
;属性仍是NAME_IN_URL / NUM_ROUNDS / PLAYERS_PER_GROUP
。 - 模板命名:模板文件名需与
Page/WaitPage
类名一致(Intro → Intro.html
),并放在templates/你的app名/
下。 - 页面顺序:
page_sequence = [PageA, PageB, ...]
放在文件末尾。
四、页面方法:全面“静态方法风格”
常用方法对照(旧 → 新):
is_displayed(self)
→@staticmethod is_displayed(player)
vars_for_template(self)
→@staticmethod vars_for_template(player)
before_next_page(self, timeout_happened)
→@staticmethod before_next_page(player, timeout_happened)
error_message(self, values)
→@staticmethod error_message(player, values)
get_form_fields(self)
→@staticmethod get_form_fields(player)
(动态表单时用)get_timeout_seconds(self)
→@staticmethod get_timeout_seconds(player)
js_vars(self)
→@staticmethod js_vars(player)
app_after_this_page(self, upcoming_apps)
→@staticmethod app_after_this_page(player, upcoming_apps)
WaitPage 对照:
after_all_players_arrive(self)
→@staticmethod after_all_players_arrive(group)
is_displayed(self)
→@staticmethod is_displayed(player)
示例:动态表单字段与超时
class Decide(Page):
@staticmethod
def get_form_fields(player: Player):
# 根据轮次动态变更
return ['age'] if player.round_number == 1 else []
@staticmethod
def get_timeout_seconds(player: Player):
return 60
五、会话与匹配:creating_session 等“钩子”的新写法
旧版很多逻辑写在类方法里(比如 Subsession.creating_session
)。新写法直接写模块级函数:
def creating_session(subsession: Subsession):
# 开场分组、处理 treatment、读取 session.config 等都在这里
if subsession.round_number == 1:
subsession.group_randomly()
管理员报告:
def vars_for_admin_report(subsession: Subsession):
players = subsession.get_players()
return dict(n_players=len(players))
六、Currency 的推荐写法:cu()
- 推荐:
C.ENDOWMENT = cu(100)
- 模板里直接
{{ C.ENDOWMENT }}
会自动以货币格式显示 - Python 中参与加减乘除时与数字类似,但保持货币类型
示例:
class C(BaseConstants):
ENDOWMENT = cu(100)
CONVERSION_RATE = 0.2
class Player(BasePlayer):
payoff_tokens = models.CurrencyField(initial=cu(0))
class Results(Page):
@staticmethod
def vars_for_template(player: Player):
cash = player.payoff_tokens * C.CONVERSION_RATE
return dict(cash=cash)
七、模板层的小技巧
- 一键渲染所有字段:
{{ formfields }}
;渲染单个字段:{{ formfield 'age' }}
- 下一步按钮:
{{ next_button }}
- 常见对象可直接用:
player / group / subsession / session / C
- 判断与循环使用 Jinja 语法:
{% if %}
、{% for %}
示例(显示分组编号):
组号:{{ player.group.id_in_subsession }}
八、分组与到达式匹配(group_by_arrival_time)
等候页里仍然用类属性:
class Match(WaitPage):
group_by_arrival_time = True
@staticmethod
def after_all_players_arrive(group: Group):
# 所有本组玩家到齐后执行
for p in group.get_players():
p.payoff = cu(10)
九、从旧项目迁移:一张“替换清单”
- 合并文件:把
models.py
、pages.py
内容合并到__init__.py
。 - 常量类重命名:
class Constants → class C
。 - 方法改静态:
self → player
(Page),self → group
(WaitPage),self → subsession
(admin report)- 如
self.player.age → player.age
;self.group → player.group
creating_session
:- 旧:
class Subsession(BaseSubsession): def creating_session(self): ...
- 新:
def creating_session(subsession): ...
- 旧:
- 货币:
- 旧:
from otree.api import Currency as c
→ 新:from otree.api import cu
;把c(...)
改为cu(...)
- 旧:
- 模板路径:确认模板放在
templates/你的app名/
,文件名与 Page 类对齐 - 导入:推荐
from otree.api import *
(省事、版本兼容好)
十、常见坑位与排查
- 模板找不到:通常是模板名与 Page 类名不一致,或放错目录(要放在
templates/你的app名/
)。 - 忘记
@staticmethod
:页面方法没加装饰器会报错,或者self
未定义。 values
使用:error_message(player, values)
里values
是用户提交的数据 dict,字段名要和form_fields
一致。- 超时逻辑:
get_timeout_seconds
返回None
表示不设定超时;before_next_page
的timeout_happened
参数用来区分是否因超时跳转。 - 机器人/自测:Bots 写法略有差异,升级后参考官方示例;确保
yield PageName, dict(...)
的字段名与form_fields
对齐。
十一、一个稍完整的演示(含 WaitPage 与结算)
from otree.api import *
class C(BaseConstants):
NAME_IN_URL = 'pg'
PLAYERS_PER_GROUP = 4
NUM_ROUNDS = 1
ENDOWMENT = cu(20)
MPCR = 0.5
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
total_contrib = models.CurrencyField()
class Player(BasePlayer):
contrib = models.CurrencyField(min=0, max=C.ENDOWMENT, label='你愿意投入公共账户的金额')
def creating_session(subsession: Subsession):
if subsession.round_number == 1:
subsession.group_randomly()
class Contribute(Page):
form_model = 'player'
form_fields = ['contrib']
class WaitAll(WaitPage):
@staticmethod
def after_all_players_arrive(group: Group):
total = sum(p.contrib for p in group.get_players())
group.total_contrib = total
for p in group.get_players():
private = C.ENDOWMENT - p.contrib
share = total * C.MPCR / C.PLAYERS_PER_GROUP
p.payoff = private + share
class Results(Page):
@staticmethod
def vars_for_template(player: Player):
g = player.group
return dict(
total=g.total_contrib,
my_contrib=player.contrib,
payoff=player.payoff,
)
page_sequence = [Contribute, WaitAll, Results]
模板 Contribute.html
:
{% block title %}公共品决策{% endblock %}
{% block content %}
你的初始资金:{{ C.ENDOWMENT }}
{{ formfields }}
{{ next_button }}
{% endblock %}
模板 Results.html
:
{% block title %}结果{% endblock %}
{% block content %}
本组总投入:{{ total }}<br>
你的投入:{{ my_contrib }}<br>
你的收益:{{ payoff }}
{{ next_button }}
{% endblock %}
结语
新写法的核心就三点:单文件、静态方法、cu()
。上手成本不高,但能让代码更短更清晰。若你是从老项目迁移,先把“替换清单”过一遍,再逐页跑通;若是新项目,直接按“最小示例”搭骨架,边写边跑最省心。
如果你已经在用新写法,也欢迎把你遇到的坑留言交流。祝大家实验顺利、部署无坑、数据好看!