Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Partially Evaluating Python Classmethod Based on Where It's Accessed From

Tags:

python

I have what is essentially the following in python:

class X(object): pass

class Y(object):
  @classmethod
  def method(cls, x_inst, *args, **kwargs):
    #do some work here that requires an instance of x
    pass

What I would like to do is add a dynamic property to all instances of X allowing acces to Y that implicitly fills in the first parameter of the method with the given instance. E.G I would like the following code to work identically:

# current
x = X()
result = Y.method(x, 1, 2, 3)

# desired
x = X()
x.Y.method(1, 2, 3)

There are several methods on several subclasses that I would like to implement this behaviour for. What I have done currently is to create a YProxy class that X actually returns, and then put split some of the code into that. It seems rather inelegant and hard to maintain however:

class X(object):
  @property
  def Y(self):
    return YProxy(self)

class Y(object):
  @classmethod
  def method(cls, x_inst, *args, **kwargs):
    #do some work here that requires an instance of x
    pass

class YProxy(object):
  def __init__(self, x_inst):
    self.x_inst = x_inst

  def method(self, *args, **kwargs):
    return Y.method(self.x_inst, *args, **kwargs)

Is there any way to conditionally partially evaluate the classmethods on an object?

like image 538
dave mankoff Avatar asked Jan 21 '12 22:01

dave mankoff


People also ask

What is Classmethod Python?

What is Class Method in Python. Class methods are methods that are called on the class itself, not on a specific object instance. Therefore, it belongs to a class level, and all class instances share a class method. A class method is bound to the class and not the object of the class. It can access only class variables ...

What is CLS () in Python?

cls accepts the class Person as a parameter rather than Person's object/instance. Now, we pass the method Person. printAge as an argument to the function classmethod . This converts the method to a class method so that it accepts the first parameter as a class (i.e. Person).

Is class method can access only class attributes?

Class methods don't need a class instance. They can't access the instance ( self ) but they have access to the class itself via cls .

How do you call a class method in Python?

To call a class method, put the class as the first argument. Class methods can be can be called from instances and from the class itself. All of these use the same method. The method can use the classes variables and methods.


1 Answers

It can be done with a Descriptor object + a wrapper class ad explicitly declaring the classes you need to wrap on this way on your target class.

A descriptor object is any object defining a __get__ method, which allows one to customize the attribute retrieving when the descriptor is part of a class. On this case, we want that when that attribute - which is the "Y" class - is retrieved from an instance, whenever a method is retrieved from that class, the instance is inserted on the parameter list.

This requires that the attribute retrieved be itself a "proxy" class with custom attribute access to allow the dynamic wrapping to take note.

Translating all this into Python, we have:

import types
from functools import partial

class APIWrapper(object):
    def __init__(self, apicls, instance):
        self._apicls = apicls
        self._instance = instance
    def __getattribute__(self, attr):
        apicls = object.__getattribute__(self, "_apicls")
        instance = object.__getattribute__(self,"_instance")
        obj = getattr(apicls, attr)
        if isinstance(obj, types.MethodType):
            return partial(obj,instance)
        return obj


class APIProperty(object):
    def __init__(self, cls):
        self.cls = cls
    def __get__(self, instance, cls):
        return APIWrapper(self.cls, instance)

class Y(object):
    @classmethod
    def method(cls, x, *args):
        print cls, x, args

class X(object):
    Y = APIProperty(Y)

#Example usage: 
x = X()
x.Y.method(1,2,3)

(prints <class '__main__.Y'> <__main__.X object at 0x18ad090> (1, 2, 3) when run)

But I suppose you don't want to need to write

Y = APIWrapper(Y) 

for each of the classes you want to wrap on this way. (And have those classes defined after the wrapped class so that Y already has been parsed when X body is parsed).

This can be done with metaclasses, class decorators, which would have to be defined for each class you'd want to apply the methods - instead, I made a function that is to be called at the end of the module definition, where you define your "X" class - this function will add the desired classes as attributes for each class defined (in my example, I want the class to be marked with an "auto_api" attribute - but suit yourself) - Thus, the "auto_api" function, the definition of the X and Y classes becomes like this (using the same APIProperty and APIWrapper as above)

def auto_api(api_classes, glob_dict):
    for key, value in glob_dict.items():
        if isinstance(value, type) and hasattr(value, "auto_api"):
            for api_class in api_classes:
                setattr(value, api_class.__name__, APIProperty(api_class))


class X(object):
    auto_api = True

class Y(object):
    @classmethod
    def method(cls, x, *args):
        print cls, x, args

auto_api((Y,), globals())

#Example

x = X()
x.Y.method(1,2,3)
like image 114
jsbueno Avatar answered Oct 11 '22 15:10

jsbueno