Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selector objects shared across instances and class #746

Closed
maximlt opened this issue Apr 17, 2023 · 4 comments · Fixed by #826
Closed

Selector objects shared across instances and class #746

maximlt opened this issue Apr 17, 2023 · 4 comments · Fixed by #826
Assignees
Labels
type-bug Bug report
Milestone

Comments

@maximlt
Copy link
Member

maximlt commented Apr 17, 2023

When check_on_set is set to False, objects is updated in place and the change is seen at both the instance and class level.

image

import param

class P(param.Parameterized):
    s = param.Selector(objects=[1, 2], check_on_set=False)

p = P()

p.s = 3
p.param.s.objects

P.param.s.objects

P.s = 4
P.param.s.objects

p.param.s.objects
@maximlt maximlt added the type-bug Bug report label Apr 17, 2023
@maximlt maximlt added this to the 2.0 milestone Jul 13, 2023
@maximlt maximlt changed the title objects updated in place when check_on_set is False Selector objects shared across instances and class Jul 19, 2023
@maximlt
Copy link
Member Author

maximlt commented Jul 19, 2023

While the original post focuses on check_on_set, it's just an outcome of the _objects attribute (objects being now a property using _objects) being shared across instances and classes:

import param

class P(param.Parameterized):
    s = param.Selector(objects=[1, 2])

p1 = P()
p2 = P()

assert p1.param.s._objects is p2.param.s._objects is P.param.s._objects

I was a bit surprised by that when I started to think about per_instance being True by default. Except that the Parameter copy each instance holds is merely a shallow copy of the class Parameter, this is by design (#306), and it explains they all just hold a reference to the same list/dict _objects.

I'm not sure how to approach this. We could maybe override Selector.__copy__ or __getstate__ to make sure _objects is shallow copied when new Parameter instances. That's a more general problem though, i.e. mutable Parameter attributes, so it may require a more general solution. The question of backwards compatibility also holds.


I suggest we push this to post Param 2.0 given the breadth of implications "fixing" that could have. cc @jbednar

@jbednar
Copy link
Member

jbednar commented Jul 21, 2023

Seems like you're suggesting the same approach as I just proposed over in #793, i.e. shallow copying Parameter slots, which I think can be done in general (not just for Selectors) unless Selectors truly are the only Parameters with mutable slot values. I think we should at least try that to see if it raise any unexpected problems, because this is a subtle change to the semantics that I think will be a bugfix, but which is one to make in 2.0 if possible if it has implications.

@maximlt
Copy link
Member Author

maximlt commented Jul 30, 2023

I started to look into that and had a hackish implementation I ran across Panel's test suite. Ignoring some horrible failures that might be due to the hackish implementation, I found one failure that is relevant to that issue as it's caused by a test that assumed objects could be updated on a class Parameter in order to affect already created instances (comment is mine):

def test_switch_param_subobject(document, comm):
    class Test(param.Parameterized):
        a = param.ObjectSelector()

    o1 = Test(name='Subobject 1')
    o2 = Test(name='Subobject 2')
    # class Parameter 'a' modified and expected to affect the instance Parameter 'a' on o1 and o2
    Test.param['a'].objects = [o1, o2, 3]
    test = Test(a=o1, name='Nested')
    test_pane = Param(test)
    model = test_pane.get_root(document, comm=comm)

Preview:
image

My question to @jbednar and @philippjfr, is Panel leveraging a Param bug or a Param feature in this test?

@maximlt maximlt self-assigned this Aug 21, 2023
@jbednar
Copy link
Member

jbednar commented Sep 8, 2023

I believe #826 addresses the issues above:

import param

class P(param.Parameterized):
    s = param.Selector(objects=[1, 2], check_on_set=False)

p = P()

p.s = 3
print(P.param.s.objects, p.param.s.objects)
# [1, 2] [1, 2, 3]

P.s = 4
print(P.param.s.objects, p.param.s.objects)
# [1, 2, 4] [1, 2, 3]

(i.e., the class and instance objects lists are now independent)

class Test(param.Parameterized):
    a = param.ObjectSelector()

o1 = Test(name='Subobject 1')
o2 = Test(name='Subobject 2')
# class Parameter 'a' modified and expected to affect the instance Parameter 'a' on o1 and o2
Test.param['a'].objects = [o1, o2, 3]
test = Test(a=o1, name='Nested')

print(o1.param.a.objects)
# [Test(a=None, name='Subobject 1'), Test(a=None, name='Subobject 2'), 3]
print(o2.param.a.objects)
# [Test(a=None, name='Subobject 1'), Test(a=None, name='Subobject 2'), 3]
print(test.param.a.objects)
# [Test(a=None, name='Subobject 1'), Test(a=None, name='Subobject 2'), 3]

(I.e., changes to objects at the class level still apply to the instances, because per_instance copying is lazy and a has not been changed on the o1 and o2 instances)

So I think #826 fixes #746.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug Bug report
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants