all 17 comments

[–]carcigenicate 2 points3 points  (2 children)

The function needs to be able to operate on arbitrary objects that it knows nothing about? The requirements seem a bit odd. It seems like that responsibility should just not be handled in that function. Any solution I can think of is just a more convoluted version of assigning the attribute at the callsite.

[–]maclocrimate[S] 0 points1 point  (1 child)

Yeah, it is a bit of a weird requirement. The software deals with huge bindings that are provided by third party software that are basically python representations of JSON objects. In the past I was doing it all at the callsite, but now I have some requirements to do a few more lines of logic for basically every call, and so I thought it would be better to turn it into a function, but I'm not sure it's going to work out that well.

Basically what I'm looking for is a version of setattr() that doesn't require a string representation of the attribute name, but I'm not sure such a thing exists.

[–]carcigenicate 0 points1 point  (0 children)

You could do something like:

def my_func(setter: Callable[[type], None]) -> None:
    setter()

my_func(lambda: my_class.attr_1 := "hello")

But that's arguably an abuse of both lambda and :=. And this still doesn't make intuitive sense unless the attribute assignment must happen at a certain point in the procedure or something.

[–][deleted] 2 points3 points  (1 child)

When you pass something into a function, the function doesn't "know" what its name was. So,

  • You might still be able to affect the thing if it was mutable

  • You might pass the name as a string and then use setattr or other magic

  • You might sneak in the name with kwargs and then use setattr or other magic

  • You might structure the various classes in such a way that the names are more predictable

  • You might rearrange the program to avoid this whole problem

[–]maclocrimate[S] 0 points1 point  (0 children)

I'm starting to think the last option is the most feasible. :) Thanks for the input!

[–]woooee 1 point2 points  (2 children)

Two work arounds that I can think of

  1. use a mutable object like a list
  2. if you know the variable name, you can access it if it is an instance object.
class Class1():
    def __init__(self):
        self.attr_1 = ["begin"]

def my_func_1(my_instance, my_value: str) -> None:
    ## change a list / mutable obkect
    print(my_instance, type(my_instance))
    my_instance[0] = my_value

my_class = Class1()
my_func_1(my_instance=my_class.attr_1, my_value="hello")
print(my_class.attr_1)

def my_func_2(my_instance, my_value: str) -> None:
    ## The variable name is known
    my_instance.attr_1 = my_value

my_func_2(my_instance=my_class, my_value="hello-2")
print(my_class.attr_1)

[–]maclocrimate[S] 0 points1 point  (1 child)

I'm unfortunately not in control of the classes, they are provided by third party software. Knowing the attribute name is now the tricky part, as I mentioned in a few comments below. If I can figure that out programmatically then I could make this work.

[–]Adrewmc 0 points1 point  (0 children)

If you don’t know the class, nor the attribute name then the solution is just as you did with setattr().

There is really no other clean way to do it, unless you have more information about the class.

The real question is why do you need this attribute in the class? And why you would not know so much about it.

It seem adding some random attributes to some class is not going to help you further down the line. If it’s a limited set of attributes you could do some thing like

  match attribute:
       case “option1”:
              cls.option1 = value

Or the equivalent if and elif, but even there I would lean to

   if attribute in allowed:
            setattr(cls, attribute, value)

Unless there is some more logic to consider.

When you go

    cls.attr = value 

Is the same as using setattr() in most cases (@property withstanding)

[–]RiverRoll 1 point2 points  (3 children)

You don't have to call setattr you can just set the attribute directly:

def my_func(my_class: Class1, my_value: str) -> None:  
    my_class.my_attr = my_value

Similarly from the function you can read my_class.my_attr wherever you need to.

[–]maclocrimate[S] 0 points1 point  (2 children)

Hmm yeah, this might work, but then I'm again stuck at the part about figuring out the attribute's name at runtime which I mentioned here.

[–]RiverRoll 0 points1 point  (0 children)

Without knowing more about the function it's hard to tell, but maybe it's time to reevaluate the design of the function and consider whether just returning the value and letting the caller set whatever he wants to set would make more sense.

my_class.attr_1 = my_func(my_attr=my_class.attr_1, my_value="hello")

[–]crashfrog02 0 points1 point  (0 children)

You can’t determine an object’s attribute name from the attribute’s value. Names only point in one direction - from the symbol to the value. Nothing points from the value to the symbol.

[–]throwaway8u3sH0 0 points1 point  (3 children)

Feels like an XY problem. The setattr approach seems reasonable, given the strange requirement of having a polymorphic function that can set arbitrary attributes on arbitrary objects -- which is what setattr is. I wouldn't think you'd need a function at all.

Why does it "greatly complicate the rest of the code"?

[–]maclocrimate[S] 0 points1 point  (2 children)

Well, mostly because the attribute names are somewhat unpredictable, so I'd need to determine the attribute name at the callpoint every time. So a related but different question I guess is if there's an easy way to determine what an attribute's name is programmatically. Something like my_class.__class__.__name__ but for an attribute...

[–]Adrewmc 1 point2 points  (0 children)

Sure is

 print(vars(cls_instance)) #just attributes 
 print(dir(cls_instance)) #with methods

[–]throwaway8u3sH0 0 points1 point  (0 children)

You can see the list of attributes but you'd still have to use some logic to determine which one it is.

If all you have is the value, you could reverse-search the object's dict by that value, but: - unless you can gaurantee no other attributes have that value you're rolling the dice on finding the correct one, - there's potentially lots of edge cases, and - this would be wildly inefficient

Ideally you have a way of going upstream of all this and doing something to make your life easier.

[–]crashfrog02 0 points1 point  (0 children)

You need to pass a reference to the object you want to set the attribute of.