aensm
aensm

Reputation: 3607

Python - Passing object value instead of reference

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?

EDIT

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

Answers (4)

hpaulj
hpaulj

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

Blckknght
Blckknght

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

Eli Korvigo
Eli Korvigo

Reputation: 10503

This happens, because in Python functions (including lambdas) 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

sardok
sardok

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

Related Questions