Consejos y trucos
Prevención de la duplicación de código
En la medida de lo posible, es bueno evitar copiar y pegar el mismo código en múltiples lugares. Aunque a veces requiere un poco de reflexión para averiguar cómo evitar copiar y pegar código, verás que tener tu código en un solo lugar generalmente te ahorra mucho esfuerzo más adelante cuando necesites cambiar el diseño de tu código o corregir errores.
A continuación se presentan algunas técnicas para lograr la reutilización de código.
No hagas múltiples copias de tu aplicación.
Si es posible, deberías evitar copiar la carpeta de una aplicación para hacer una versión ligeramente diferente, porque así habrás duplicado código que es más difícil de mantener.
Si necesitas múltiples rondas, establece NUM_ROUNDS. Si necesitas versiones ligeramente diferentes (por ejemplo, tratamientos diferentes), entonces deberías usar las técnicas descritas en Tratamientos, como crear 2 configuraciones de sesión que tengan un parámetro 'treatment' diferente y luego verificar session.config['treatment'] en el código de tu aplicación.
Cómo crear muchos campos
Supongamos que tu aplicación tiene muchos campos que son casi iguales, como:
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...
Esto es bastante complejo; deberías buscar una manera de simplificar.
¿Se muestran todos los campos en páginas separadas? Si es así, considera convertir esto en un juego de 10 rondas con solo un campo.
Si eso no es posible, puedes reducir la cantidad de código repetido definiendo una función que devuelva un campo:
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.')
Evita páginas duplicadas utilizando múltiples rondas
Si tienes muchas páginas que son casi iguales, considera tener solo 1 página y hacer un bucle para múltiples rondas. Una señal de que tu código puede simplificarse es si se ve algo así:
# [pages 1 through 7....]
class Decision8(Page):
form_model = 'player'
form_fields = ['decision8']
class Decision9(Page):
form_model = 'player'
form_fields = ['decision9']
# etc...
Evita métodos de validación duplicados
Si tienes muchos métodos repetitivos de FIELD_error_message, puedes reemplazarlos con una única función error_message. Por ejemplo:
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'
En su lugar, puedes definir esta función en tu página:
@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
(Normalmente error_message se utiliza para devolver un único mensaje de error como una cadena, pero también puedes devolver un diccionario.)
Evitar funciones de página duplicadas
Cualquier función de página puede ser movida fuera de la clase de página y convertirse en una función de nivel superior. Esta es una forma práctica de compartir la misma función entre múltiples páginas. Por ejemplo, supongamos que muchas páginas necesitan tener estas 2 funciones:
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()
Puedes mover esas funciones antes de todas las páginas (eliminar el @staticmethod), y luego referenciarlas donde sea necesario:
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
(En los juegos de muestra, after_all_players_arrive y live_method se definen frecuentemente de esta manera.)
Mejorando el rendimiento del código
Debes evitar el uso redundante de get_players(), get_player_by_id(), in_*_rounds(), get_others_in_group() o cualquier otro método que devuelva un jugador o una lista de jugadores. Todos estos métodos requieren una consulta a la base de datos, lo que puede ser lento.
Por ejemplo, este código tiene una consulta redundante porque le pide a la base de datos 5 veces el mismo jugador:
@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
)
Debería simplificarse a esto:
@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
)
Como beneficio adicional, esto generalmente hace que el código sea más legible.
Utiliza BooleanField en lugar de StringField, cuando sea posible
Muchos StringFields deberían descomponerse en BooleanFields, especialmente si solo tienen 2 valores distintos.
Supongamos que tienes un campo llamado treatment:
treatment = models.StringField()
Y digamos que treatment solo puede tener 4 valores diferentes:
high_income_high_taxhigh_income_low_taxlow_income_high_taxlow_income_low_tax
En tus páginas, podrías usarlo así:
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'
Sería mucho mejor dividir esto en 2 BooleanFields separados:
high_income = models.BooleanField()
high_tax = models.BooleanField()
Entonces tus páginas podrían simplificarse a:
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
Si accedes a un campo de Player/Group/Subsession cuyo valor es None, oTree generará un TypeError. Esto está diseñado para detectar situaciones en las que un usuario olvidó asignar un valor a ese campo, o olvidó incluirlo en form_fields.
Sin embargo, a veces necesitas acceder intencionadamente a un campo cuyo valor puede ser None. Para hacer esto, utiliza field_maybe_none, que suprimirá el error:
# instead of player.abc, do:
abc = player.field_maybe_none('abc')
# also works on group and subsession
Nota
field_maybe_none es nuevo en oTree 5.4 (agosto de 2021).
Una solución alternativa es asignar un valor inicial al campo para que su valor nunca sea None:
abc = models.BooleanField(initial=False)
xyz = models.StringField(initial='')