-
-
Notifications
You must be signed in to change notification settings - Fork 74
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
Why force instantiate for constant=True? #287
Comments
I definitely think so, as you say it should be selecting between existing objects not making new copies. In fact I would have inspected the validation to fail because the copy is not one of the existing objects. |
Hmm, yes, I can see how that's a problem. I can't get back into param justnow while I'm in the middle of the current pyct/nbsite stuff, but I can say that instantiate happens for constant to avoid the normal behavior where setting on the class would change the value on an existing instance. The problem is more to do with instantiate always meaning copy (which was presumably done for other mutable things, such as lists) as opposed to being only about the value existing in instance dict vs parameter.default. So modifying how instantiate works for object selector seems like a good idea to me (or having general support for saying something about copying alongside instantiate, which could then be set appropriately for object selector).
As faras I remember, the default is not validated unless objects are supplied (no objects supplied in example above). Not sure how well tested, though. |
Yes, thanks, didn't look at the example closely enough. |
xref #28 |
This still seems like it needs addressing. |
Just some comments to make this issue a little easier to understand. The normal Param behavior when import param
class P(param.Parameterized):
x = param.Parameter(1)
p = P()
assert p.x == 1
# Changing the class value (Parameter default)
P.x = 10
# affects the instance if the value hasn't yet been set (instantiation or setattr)
assert p.x == 10 With import param
class P(param.Parameterized):
x = param.Parameter(1, constant=True)
p = P()
assert p.x == 1
# Changing the value on the class
P.x = 10
# No longer affects the instance
assert p.x == 1
# New instances
new_p = P()
# obviously get the updated class value
assert new_p.x == 10 We had a discussion about this yesterday. @jbednar and/or @philippjfr could you please chime in and describe further what you believe should be the way forward here? Out of curiosity I listed the Parameters that have
Incidentally, constant=True leading to instantiate=True is currently broken on the main branch as a consequence of the work on |
Ok, let's try to sort this out with examples. First, let's get a list of some objects that have identity and internal state: import param
class A(param.Parameterized):
a = param.Number(0)
obj1 = A(name="Obj_1", a=1)
obj2 = A(name="Obj_2", a=2)
objects = [obj1, obj2]
objects ![]() Here Now let's define a parameter whose values is meant to be either class B(param.Parameterized):
c = param.Selector(default=objects[0], objects=objects)
bb = B() My expectation is that print(bb.c) ![]() So far, so good. What if I want B's parameter class B(param.Parameterized):
c = param.Selector(default=objects[0], objects=objects, constant=True)
bb = B()
bb.c = objects[1] ![]() But I still expect that the value of bb = B()
print(bb.c, bb.c in objects) ![]() To me, that behavior is not just suprising but entirely wrong, and it prevents From the comments in the code, I think what happened here was that someone was trying to ensure that constant parameters were immune to changes in that parameter in a superclass: class C(param.Parameterized):
c = param.Selector(default=objects[0], objects=objects, constant=True)
class B(C):
pass
cc = B()
print(cc.c)
C.c = objects[1]
print(cc.c, objects[1]) ![]() I.e., I think the upshot is that So, are there cases where deepcopying is appropriate, or was that always a mistake for options = [1, 2, 3, 4]
class E(param.Parameterized):
e = param.List(options)
ee = E()
print(ee.e) ![]() ee.e += [5]
print(ee.e)
print(options) ![]() As you can see, the options = [1, 2, 3, 4]
class E(param.Parameterized):
e = param.List(options, instantiate=False)
ee = E()
ee.e += [5]
print(ee.e) ![]() With that in mind, deepcopying does seem reasonable by default for a non-Selector type like List, Hooklist, Dict, Array, DataFrame, and Series; in each case the Parameter accepts an object with state, and I don't think (unlike with Selectors) there is any implication that it is a specific object with identity. So it seems better to prioritize avoiding surprise by having the state shared as in the List exampleg. For ClassSelector (the other Parameter type where So, where does that leave us? I think it leaves us needing to make a distinction between a non-deepcopy What I propose is that we need to separate instantiation from deepcopying. My guess is that the least disruptive way to do that is to leave Note that Also note that I called it How does this proposal sound? Did I get confused anywhere in this long chain of reasoning? |
The regression that affected the behavior described in this issue was fixed in #771. |
Thanks for the excellent writeup, I agree with everything you said until the point where you started talking about I would then also disable |
It seems like for Parameters that refers mutable objects, you need a mechanism to make sure you're not going to share the attribute across all instances of the Parameterized class. Some other libraries, like from attrs import define, field
@define
class C:
a: list = field(factory=list)
C()
# C(a=[]) Param has chosen the opposite approach I think, making it less likely for its users not to shoot themselves in the foot, by automatically deepcopying the default value for these kinds of Parameter (e.g. List, Dict). class P(param.Parameterized):
# not shared as instantiate is True by default, to deepcopy the default object when an instance is created
x = param.List([])
# shared
y = param.List([], instantiate=False) which would be equivalent? to an hypothetical: class P(param.Parameterized):
# not shared (would it be called to set the class value?)
x = param.List(factory=list)
# shared, assuming instantiate is no longer True by default for List by False
y = param.List([]) Certainly having a factory attribute ( |
|
Sure, but if you set constant it's forced to True, right? |
Yes! |
Yes, if you set
I think the correct name would be
I'd reformulate this as a question: Should So my guess is that we should go with proposal (a) even though (b) is arguably clearer behavior. (b) makes class Parameter(_ParameterBase):
instantiate: controls whether the value of this Parameter will
be deepcopied when a Parameterized object is instantiated (if
True), or if the single default value will be shared by all
Parameterized instances (if False). For an immutable Parameter
value, it is best to leave instantiate at the default of
False, so that a user can choose to change the value at the
Parameterized instance level (affecting only that instance) or
at the Parameterized class or superclass level (affecting all
existing and future instances of that class or superclass). For
a mutable Parameter value, the default of False is also appropriate
if you want all instances to share the same value state, e.g. if
they are each simply referring to a single global object like
a singleton. If instead each Parameterized should have its own
independently mutable value, instantiate should be set to
True, but note that there is then no simple way to change the
value of this Parameter at the class or superclass level,
because each instance, once created, will then have an
independently instantiated value. I.e., because our docs previously associated deepcopying with instantiate, my vote is for proposal (a) so that we are simply adding a new, optional way to decouple deepcopying from instantiate, without changing the semantics of what people previously wrote. Anyone want to argue for (b) instead, the purist option that's a breaking change? |
I agree having clearer and better documented and more universal support for factory attributes would be nice to have, but I think that's orthogonal. I don't think we should alias |
I'm confused by this behavior:
<A AAA> <A AAA> <A AAA> <A AAA> <A A00004>
I don't see why bb.c differs from any of the other objects shown -- why is it not the specific object that I supplied for parameter
b
, and instead is a new copy of that object? This was just the source of a very confusing behavior when using ObjectSelector.The code involved states that constant parameters must be instantiated, but I don't see why that should be true for ObjectSelector, which when instantiate=False (the default) is meant to select between existing objects. Should we start overriding this general
constant=True
behavior to avoid it for ObjectSelector?The text was updated successfully, but these errors were encountered: