简体   繁体   中英

Dynamically add method to class from property function?

I think a code sample will better speak for itself:

class SomeClass:
    example = create_get_method()

Yes, that's all – ideally.

In that case, create_get_method would add a get_example() to SomeClass in a way that it can be accessed via an instance of SomeClass :

obj = SomeClass()
obj.get_example() <- returns the value of self.example

(Of course, the idea is to implement a complex version of get_contact , that's why I want to do that in a non-repetitive way, and this is a simplified version that represents well the issue.)

I don't know if that's possible, because it require to have access to the property name ( example ) and the class ( SomeClass ) since these can not be guessed in advance (that function will be used on many and various classes).

I know it's something possible, because that's kind of what SQLAlchemy does with their relationship() function on a class:

class Model(BaseModel):
    id = ...
    contact_id = db.Integer(db.ForeignKey..)
    contact = relationship('contact') <-- This !

How can this be done?

Objects bound to class-level variables can have a __set_name__ method that will be called immediately after the class object has been created. It will be called with two arguments, the class object, and the name of the variable the object is saved as in the class.

You could use this to create your extra getter method, though I'm not sure why exactly you want to (you could make the object a descriptor instead, which would probably be better than adding a separate getter function to the parent class).

class create_get_method:
    def __set_name__(self, owner, name):
        def getter(self):
            return getattr(self, name)

        getter_name = f"get_{name}"
        getter.__name__ = getter_name
        setattr(owner, getter_name, getter)

    # you might also want a __get__ method here to give a default value (like None)

Here's how that would work:

>>> class Test:
...    example = create_get_method()
...    

>>> t = Test()

>>> print(t.get_example())
<__main__.create_get_method at 0x000001E0B4D41400>

>>> t.example = "foo"

>>> print(t.get_example())
foo

You could change the value returned by default (in the first print call), so that the create_get_method object isn't as exposed. Just add a __get__ method to the create_get_method class.

You can do this with a custom non-data descriptor, like a property, except that you don't need a __set__ method:

class ComplicatedDescriptor:
    def __init__(self, name):
        self.name = name

    def __get__(self, owner, type):
        # Here, `owner` is the instance of `SomeClass` that contains this descriptor
        # Use `owner` to do some complicated stuff, like DB lookup or whatever
        name = f'_{self.name}'

        # These two lines for demo only
        value = owner.__dict__.get(name, 0)
        value += 1

        setattr(owner, name, value)
        return value

Now you can have any number of classes that use this descriptor:

class SomeClass:
    example = ComplicatedDescriptor('example')

Now you can do something like:

>>> inst0 = SomeClass()
>>> inst1 = SomeClass()
>>> inst0.example
1
>>> inst1.example
1
>>> inst1.example
2
>>> inst0.example
2

The line name = f'_{self.name} is necessary because the descriptor here is a non-data descriptor: it has no __set__ method, so if you create inst0.__dict__['example'] , the lookup will no longer happen: inst0.example will return inst0.__dict__['example'] instead of calling SomeClass.example.__get__(inst0, type(inst0)) . One workaround is to store the value under the attribute name _example . The other is to make your descriptor into a data descriptor:

class ComplicatedDescriptor_v2:
    def __init__(self, name):
        self.name = name

    def __get__(self, owner, type):
        # Here, `owner` is the instance of `SomeClass` that contains this descriptor
        # Use `owner` to do some complicated stuff, like DB lookup or whatever

        # These two lines for demo only
        value = owner.__dict__.get(self.name, 0)
        value += 1

        owner.__dict__[self.name] = value
        return value

    def __set__(self, *args):
        raise AttributeError(f'{self.name} is a read-only attribute')

The usage is generally identical:

class SomeClass:
    example = ComplicatedDescriptor_v2('example')

Except that now you can't accidentally override your attribute:

>>> inst = SomeClass()
>>> inst.example
1
>>> inst.example
2
>>> inst.example = 0
AttributeError: example is a read-only attribute

Descriptors are a fairly idiomatic way to get and set values in python. They are preferred to getters and setters in almost all cases. The simplest cases are handled by the built-in property . That being said, if you wanted to explicitly have a getter method, I would recommend doing something very similar, but just returning a method instead of calling __get__ directly.

For example:

def __get__(self, owner, type):
    def enclosed():
        # Use `owner` to do some complicated stuff, like DB lookup or whatever
        name = f'_{self.name}'

        # These two lines for demo only
        value = owner.__dict__.get(name, 0)
        value += 1

        setattr(owner, name, value)
        return value
    return enclosed

There is really no point to doing something like this unless you plan on really just want to be able to call inst.example() .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM