Reputation: 3607
In Python, as I understand it, variables are really references to objects in the given namespace. So in the following example, it is unsurprising that when noise
changes in the global namespace, the value returned by cat.noise
changes, as the reference in the setattr
line is using the reference of noise
, not its underlying value.
class Cat(object):
pass
noise = "meow"
setattr(Cat, "noise", property(lambda self: noise))
cat = Cat()
cat.noise
# Outputs "meow"
noise = "purrrrr"
cat.noise
# Outputs "purrrrr"
That being said, is there a way to pass the value of noise when calling setattr
as above? I figured that I could isolate the namespace by using a function, and that did work:
class Cat(object):
pass
noise = "meow"
def setproperties(cls, k, v):
setattr(cls, k, property(lambda self: v))
setproperties(Cat, "noise", noise)
cat = Cat()
cat.noise
# Outputs "meow"
noise = "purrrrr"
cat.noise
# Still outputs "meow"
Is it possible to do so without passing the object through a function (without using eval
or the like)? And as a secondary question, is my reasoning about what goes on under the hood correct?
As per the request for a less contrived example in the comments, consider the following. Imagine I am trying to dynamically set attributes in Cat
, based on the values of its friend Dog
:
class Dog(object):
noise = 'woof'
adorable = True
class Cat(object):
friend = Dog
friend_attrs = filter(lambda attr: not attr.startswith('__'), Dog.__dict__)
for attr in friend_attrs:
setattr(Cat, "friend_" + attr, property(lambda self: getattr(self.friend, attr)))
cat = Cat()
cat.friend_noise
# Outputs True
cat.friend_adorable
# Outputs True
Upvotes: 3
Views: 3978
Reputation: 231395
With
setattr(Cat, "noise", property(lambda self: noise))
just defines the property as a function that returns the value of the global noise
variable. It is similar to
def make_noise(self):
return noise
The 2nd case is a little more complicated. The property now returns the value of v
, which was defined during creation.
I can produce the same sort of behavior with a simple function:
In [268]: bar = 'barstring'
In [269]: def foo():
...: return bar # returns the global
...:
In [270]: foo()
Out[270]: 'barstring'
In [271]: bar = 'xxx'
In [272]: foo()
Out[272]: 'xxx'
but if I pass bar
as a keyword, I can lock in the value, to the current one:
In [273]: def foo1(bar=bar):
...: return bar
...:
In [274]: foo1()
Out[274]: 'xxx'
In [275]: bar = 'ooo'
In [276]: foo1()
Out[276]: 'xxx'
In [277]: foo()
Out[277]: 'ooo'
But if I make bar
mutable (e.g. a list)
In [278]: bar=['one']
In [279]: foo()
Out[279]: ['one']
In [280]: def foo1(bar=bar):
...: return bar
...:
In [281]: foo1()
Out[281]: ['one']
In [282]: bar[0]='two'
In [283]: foo1()
Out[283]: ['two']
foo1
returns an item from its __defaults__
(if I don't give it an argument). foo
continues to access the global, because there's no local binding for bar
.
In [289]: foo1.__defaults__
Out[289]: (['two'],)
In [290]: foo.__defaults__
At heart this is a question of where the variable is bound - locally, in some container function, or globally.
Mutable defaults like this can be useful, but they can also be a cause of errors.
============
Another example of local binding
In [297]: def make_foo(bar):
...: def foo():
...: print(locals())
...: return bar
...: return foo
...:
In [298]: foo2=make_foo('test')
In [299]: foo2()
{'bar': 'test'}
Out[299]: 'test'
foo
looks the same, but now bar
references the variable bound in its locals
. partial
and your setproperties
do the same thing.
Upvotes: 0
Reputation: 104712
Python functions (including lambda functions) refer to non-local variables by name. That means they'll get the latest value of that variable, not the one it had when the function was defined (indeed, the variable need not have been defined at all when the function was defined).
One way to work around this is to put the value as a default value for an argument to the function. The default value is evaluated at the function definition time, not when the function is called (this is why mutable default arguments are often problematic).
Try something like this:
for attr in friend_attrs:
setattr(Cat, "friend_" + attr,
property(lambda self, attr=attr: # added attr=attr default here!
getattr(self.friend, attr))) # attr is the lambda's arg here
Upvotes: 1
Reputation: 10503
This happens, because in Python functions (including lambda
s) use symbolic binding, i.e. your lambda
points at the variable, not at its value. To overcome this you should enclose that variable (create a closure):
noise = "meow"
f1 = lambda self: noise
f2 = (lambda x: (lambda self: x))(noise)
noise = "mur"
print(f1(None)) # -> "mur"
print(f2(None)) # -> "meow"
But you have already found that yourself by using a function to enclose the operation. This is Pythonic.
Upvotes: 1
Reputation: 1116
Just pass the value of noise to setattr
function. E.g.
class C(object):
pass
noise = 'mrrrr'
setattr(C, 'noise', noise)
c = C()
c.noise
# outputs 'mrrrr'
noise = 'qwe'
c.noise
# outputs 'mrrrr'
Edit: For case where getter function is needed for some reason.
You may use intermediate value.
class D(object):
pass
noise = 'mrrrr'
setattr(D, '_noise', noise)
setattr(D, 'noise', property(lambda self: self._noise))
d = D()
d.noise
# Outputs 'mrrrr'
noise = 'pheew'
d.noise
# Outputs 'mrrrr'
Edit 2: using partial functions.
import functools
class E(object):
pass
noise = 'mrrrr'
getter = lambda _, noise: noise
partial = functools.partial(getter, noise=noise)
setattr(E, 'noise', property(partial))
e = E()
e.noise
# Outputs 'mrrrr'
noise = 'fooooo'
e.noise
# Outputs 'mrrrr'
Upvotes: 2