4

I have a classmethod that I want to be called automatically before or when any child of the class it belongs to gets created. How can I achieve that?

Since I understand this can be an XY problem, here is why I (think I) need it, and my basic implementation.

I have a class that gets inherited, and the children of that class can specify a list of parameters that need to be converted into properties. Here is the relevant code:

class BaseData:
    @staticmethod
    def internalName(name: str) -> str:
        return '_' + name

    def __init__(self):
        for k, v in self._dataProperties.items():
            setattr(self, BaseData.internalName(k), v)
        self._isModified = False
    
    @classmethod
    def initProperties(cls):
        for k, v in cls._dataProperties.items():
            # Create dynamic getter and setter function
            getterFunc = eval(f'lambda self: self.getProperty("{k}")')
            setterFunc = eval(f'lambda self, v: self.setProperty("{k}", v)')

            # Make them a property
            setattr(cls, k, property(fget=getterFunc, fset=setterFunc))

class ChildData(BaseData):
    _dataProperties = {
        'date': None,
        'value': '0',
    }

ChildData.initProperties()

Function initProperties need to be called once for each child, and to enforce that I need to write it below function definition. I find it a bit ugly, so.. Is there any other wat to do it?

I already tried to put the code in __init__, but it does not get called when I unpickle the objects.

So, basically:

  1. Generic question: is there any way to force a function to be called the first time a child class is used (even when __init__ does not get called)?
  2. If not, more specific question: is there a way to automatically call a specific function (it can be __init__, but also another one) when unpickling?
  3. If not, single use-case question: Is there a better way to do what I'm doing here?

I already read Python class constructor (static), and while it has the same question I did not find a reply that could solve my use case.

shadowtalker
  • 12,529
  • 3
  • 53
  • 96
frarugi87
  • 2,826
  • 1
  • 20
  • 41
  • 3
    You might want to change the title to reflect your specific question, otherwise it might get flagged as a duplicate. The answer to the title question is "yes": https://stackoverflow.com/q/100003/2954547. But the actual question you propose deserves a standalone answer IMO. – shadowtalker Mar 04 '23 at 08:01
  • 2
    A metaclass can do exactly what you are asking for. On the other hand, that looks really ugly. I believe it would be better to change the whole pattern and implement a [descriptor](https://docs.python.org/3/howto/descriptor.html) to be used for each of the properties. – zvone Mar 04 '23 at 08:14
  • 3
    Are you looking for `__init_subclass__`? – MisterMiyagi Mar 04 '23 at 08:40
  • @shadowtalker thank you for your feedback; I changed it to better reflect what I wrote in the question – frarugi87 Mar 04 '23 at 14:00
  • @zvone thank you; I don't know descriptors, but I will look at the documentation and see how I can improve my code – frarugi87 Mar 04 '23 at 14:02
  • @MisterMiyagi Thank you a lot for that. Yes, that's what I was looking for, but I found a lot of additional answers below. Since this was not suggested by the others, however, it seems to me that there may be drawbacks with this solution. What do you think? – frarugi87 Mar 04 '23 at 14:17
  • @frarugi87 I actually forgot about it in my answer. There are 4 ways to do this, not 3! – shadowtalker Mar 04 '23 at 14:33
  • 1
    I have proposed a more generally applicable title. Please do feel free to roll back my edit if you don't like it. – shadowtalker Mar 04 '23 at 14:36
  • 3
    There are several ways to do it - but since Python 3.6,when `__init_subclass__` was created, this is the obvious and straightforward way. There are no drawbacks. Usually people would do so without using `eval` - but it is not that bad in the way you are using it, and even Python's library resort to similar use of `eval` at times. – jsbueno Mar 05 '23 at 00:55
  • 1
    @frarugi87 There are no drawbacks. But metaprogramming isn’t exactly something people stay up to date naturally and only for practical reasons. `__init_subclass__` is just plain, boring "does exactly what you need 99% of times in class metaprogramming"; it has none of the cool (but brittle/dangerous) parts of metaclasses or the academic generality (but need for busywork) of decorators. – MisterMiyagi Mar 05 '23 at 06:40

3 Answers3

4

To achieve the behavior you described, you can use a metaclass. A metaclass is a class that creates classes. You can define a metaclass for BaseData that calls initProperties automatically when a child class is defined.

Here is an example implementation:

class InitPropertiesMeta(type):
    def __new__(cls, name, bases, attrs):
        new_class = super().__new__(cls, name, bases, attrs)
        if name != 'BaseData':
            new_class.initProperties()
        return new_class

class BaseData(metaclass=InitPropertiesMeta):
    @staticmethod
    def internalName(name: str) -> str:
        return '_' + name

    def __init__(self):
        for k, v in self._dataProperties.items():
            setattr(self, BaseData.internalName(k), v)
        self._isModified = False
    
    @classmethod
    def initProperties(cls):
        for k, v in cls._dataProperties.items():
            # Create dynamic getter and setter function
            getterFunc = eval(f'lambda self: self.getProperty("{k}")')
            setterFunc = eval(f'lambda self, v: self.setProperty("{k}", v)')

            # Make them a property
            setattr(cls, k, property(fget=getterFunc, fset=setterFunc))

class ChildData(BaseData):
    _dataProperties = {
        'date': None,
        'value': '0',
    }

The InitPropertiesMeta metaclass overrides the new method, which is called when a new class is defined. When BaseData is defined, new is called with name equal to 'BaseData', so it does nothing. When a child class like ChildData is defined, new is called with name equal to the name of the child class, so it calls initProperties on the child class.

With this implementation, you no longer need to call initProperties explicitly for each child class. Instead, it will be called automatically when the child class is defined.

Regarding your more specific question about unpickling, you can define the getstate and setstate methods in your class to control pickling and unpickling behavior. For example, you could define getstate to return a tuple of the values you want to pickle, and setstate to set those values when unpickling. i hope this help you :)

mhmmdamiwn
  • 146
  • 9
  • Thank you a lot for your very precise answer; I will definitely look more into metaclasses (for now I only had to use the ABC metaclass, but never went too much into details) – frarugi87 Mar 04 '23 at 13:47
  • 2
    metaclasses were the only way to do that before `__init_subclass__` was created in Python 3.6 - and the reason why it exists is to enable one to skip the usage of a metaclass, as it is simpler. – jsbueno Mar 05 '23 at 00:56
4

Use __init_subclass__ instead of initProperties. (This class method—special-cased so that you don't need to decorate it with classmethod—was added to handle many of the things a metaclass would otherwise be necessary for.)

class BaseData:
    @staticmethod
    def internalName(name: str) -> str:
        return '_' + name

    def __init__(self):
        for k, v in self._dataProperties.items():
            setattr(self, BaseData.internalName(k), v)
        self._isModified = False
    
    def __init_subclass__(cls):
        for k, v in cls._dataProperties.items():
            # Create dynamic getter and setter function
            getterFunc = eval(f'lambda self: self.getProperty("{k}")')
            setterFunc = eval(f'lambda self, v: self.setProperty("{k}", v)')

            # Make them a property
            setattr(cls, k, property(fget=getterFunc, fset=setterFunc))

class ChildData(BaseData):
    _dataProperties = {
        'date': None,
        'value': '0',
    }

A custom descriptor, though, to avoid the need for eval (or similar steps to capture the current value of k and v) is cleaner.

chepner
  • 497,756
  • 71
  • 530
  • 681
3

There are several tools you can use for this:

  1. Ask Python to do something when the class is first created, e.g. dynamically creating class methods and class attributes.
  2. Use a decorator that modifies the class immediately upon creation.
  3. Use a descriptor for each attribute. For the sake of space and time I will leave this to another answer.

Option 1: Metaclass

The first solution corresponds more or less to your proposed "Y" solution. A metaclass is the type/class of a class object, and you can use this to customize the class object before it is created.

This solution is demonstrated in the other answer so I will not go into detail on its usage. However you can refer to What are metaclasses in Python? and the Python docs (1 2) for more information.

For another usage example, abstract base classes in the standard library are implemented using a metaclass. You can check out the source code if you want to see the implementation.

Option 2: Decorator

Instead of changing the type of the class and customizing its creation, you can also just write a function that modifies the class, and then call that function on the class after creating it. Python makes this very ergonomic using decorators.

def initProperties(cls):
    for k, v in cls._dataProperties.items():
        getterFunc = eval(f'lambda self: self.getProperty("{k}")')
        setterFunc = eval(f'lambda self, v: self.setProperty("{k}", v)')
        setattr(cls, k, property(fget=getterFunc, fset=setterFunc))


class BaseData():
    @staticmethod
    def internalName(name: str) -> str:
        return '_' + name

    def __init__(self):
        for k, v in self._dataProperties.items():
            setattr(self, BaseData.internalName(k), v)
        self._isModified = False


@initProperties
class ChildData(BaseData):
    _dataProperties = {
        'date': None,
        'value': '0',
    }

The popular Attrs library uses this decorator technique instead of metaclasses.

Option 3: Descriptor

For the sake of space and time, I will let another answer cover this in detail. The Python Descriptor Howto guide linked by zvone in the comments also provides comprehensive coverage of the topic.

Usage of the descriptor technique might look very similar to the "placeholders for data properties" described below:

class ChildData(BaseData):
    date = DataProperty(None)
    value = DataProperty('0')

This is arguably the most Pythonic solution!

Notes

Placeholders for data properties

Some libraries take the approach of using special placeholder objects in place of your _dataProperties dict. These libraries then walk through all class attributes, looking for (and maybe even removing) those attributes and acting as needed.

For example:

import inspect


class DataProperty:
    def __init__(self, value):
        self.value = value


def isDataProperty(obj):
    return isinstance(obj, DataProperty)


def initProperties(cls):
    for k, v in inspect.getmembers(cls, isDataProperty):
        getterFunc = eval(f'lambda self: self.getProperty("{k}")')
        setterFunc = eval(f'lambda self, v: self.setProperty("{k}", v)')
        setattr(cls, k, property(fget=getterFunc, fset=setterFunc))


class BaseData():
    @staticmethod
    def internalName(name: str) -> str:
        return '_' + name

    def __init__(self):
        for k, v in self._dataProperties.items():
            setattr(self, BaseData.internalName(k), v)
        self._isModified = False


@initProperties
class ChildData(BaseData):
    date = DataProperty(None)
    value = DataProperty('0')

More recently, libraries have been using class variable annotations for the same purpose, instead of placeholder objects. Attrs for example implements both versions.

Dynamic code generation

You almost certainly do not need or want to dynamically generate these particular functions using eval(). I am willing to assume that these are placeholders for more complicated dynamic code generation.

However, you should strongly consider alternative design patterns like factory functions, callable objects, and partial function application whenever possible.

Dynamic source code generation should be considered an absolute last resort!

shadowtalker
  • 12,529
  • 3
  • 53
  • 96
  • 1
    Wow, this answer is... too much. Thank you for your time. So in the end, if I understood correctly, the practical difference between the first two solutions is that with the first the child automatically call the initProperties function, while with the second I have to manually "decorate" all the children. Correct? Is there any other practical difference? Just one note, in the code for solution 2 the initProperties function shall return cls – frarugi87 Mar 04 '23 at 13:51
  • Regarding solution 3, I will dig deeper in the descriptors. For the moment I only implemented the first two solutions, but will definitely come back to check that guide. Regarding the placeholders, actually I need to keep additional info on the parameters, not just their value, so I will need to keep the dictionary, but thank you. – frarugi87 Mar 04 '23 at 13:53
  • And finally, regarding the dynamic code generation, I will look into your pointers. Actually the "eval" part is the correct one, so each property shall either call getProperty or setProperty, with the property name, and those two functions will do the magic, but I did not find out how to do it (i.e. create a property to call the two functions with the property name) without eval, but I will check the resources to see if I find alternative ways to do it – frarugi87 Mar 04 '23 at 13:57
  • 1
    @frarugi87 with a metaclass, you only need to mark the base class as a metaclass. With a decorator, you need to mark all subclasses with the decorator. – shadowtalker Mar 04 '23 at 14:30
  • 1
    @frarugi87 ask a separate question about better ways to implement those methods dynamically without manual code generation. @ me here with a link so I can help. – shadowtalker Mar 04 '23 at 14:31