smarie / python-pyfields

Define fields in python classes. Easily.
https://smarie.github.io/python-pyfields
BSD 3-Clause "New" or "Revised" License
45 stars 10 forks source link

Incorrect work of lists in fields. #83

Closed kolod closed 3 years ago

kolod commented 3 years ago

I want to get a list of objects. One of the fields in this object is a list. When adding items to the field of one of the objects. This element is added to the fields of all elements. More precisely, all objects use one copy of the list.

Example test:

#!/usr/bin/python3

import unittest
from pyfields import field, init_fields, make_init
from typing import List

class Test_make_init():

    test_list: List[str] = field(default=[])
    __init__ = make_init()

class Test_init_fields:

    test_list: List[str] = field(default=[])

    @init_fields
    def __init__(self) -> None:
        pass

class TestFields(unittest.TestCase):

    def test_make_init(self):

        # Prepera list
        TestObjects: List[Test_make_init] = []
        for i in range(3):
            TestObjects.append(Test_make_init())
            TestObjects[-1].test_list.append(f'{i}')

        # Check list
        for i in range(3):
            self.assertEqual(TestObjects[i].test_list, [f'{i}'])

    def test_init_fields(self):

        # Prepera list
        TestObjects: List[Test_init_fields] = []
        for i in range(3):
            TestObjects.append(Test_init_fields())
            TestObjects[-1].test_list.append(f'{i}')

        # Check list
        for i in range(3):
            self.assertEqual(TestObjects[i].test_list, [f'{i}'])

if __name__ == "__main__":
    unittest.main()
smarie commented 3 years ago

This is normal: pyfields follows strictly python's behaviour with default. I think that in your case you probably wish to use default_factory as suggested in the doc:

The behaviour with default values is the same than python's default: the same value is used for all objects. Therefore if your default value is a mutable object (e.g. a list) you should not use this mechanism, otherwise the same value will be shared by all instances that use the default

So you can use

field(default_factory=lambda obj: [])

or

field(default_factory=copy_value([]))

Let me know if this works for you.

smarie commented 3 years ago

@kolod did the above answer work for you ? I'm closing the ticket now, please feel free to reopen if this is not the case.