简体   繁体   中英

Is it possible to modify a class with a decorator

I am writing a class in python for some settings wich looks like this:

class _CanvasSettings:
    def __init__(self, **kwargs):
        super().__init__()
        self._size_x = _int(kwargs, 'size_x', 320)
        self._size_y = _int(kwargs, 'size_y', 240)
        self._lock_ratio = _bool(kwargs'lock_ratio', True)

    def get_size_x_var(self):
        return self._size_x
    def _get_size_x(self):
        return self._size_x.get()
    def _set_size_x(self, value):
        self._size_x.set(value)
    size_x = property(_get_size_x, _set_size_x)

    def get_size_y_var(self):
        return self._size_y
    def _get_size_y(self):
        return self._size_y.get()
    def _set_size_y(self, value):
        self._size_y.set(value)
    size_y = property(_get_size_y, _set_size_y)

    def get_lock_ratio_var(self):
        return self._lock_ratio
    def _get_lock_ratio(self):
        return self._lock_ratio.get()
    def _set_lock_ratio(self, value):
        self._lock_ratio.set(value)
    lock_ratio = property(_get_lock_ratio, _set_lock_ratio)

as you can see I add the block:

    def get_something_var(self):
        return self._something
    def _get_something(self):
        return self._something.get()
    def _set_something(self, value):
        self._something.set(value)
    something = property(_get_something, _set_something)

For every single setting.
Is it possible to automate this task with a decorator ?

I would like to do it like this (pseudocode):

def my_settings_class(cls):
    result = cls
    for field in cls:
        result.add_getter_setter_and_property( field )
    return result

@my_settings_class
class _CanvasSettings:
    def __init__(self, **kwargs):
        super().__init__()
        self._size_x = _int(kwargs, 'size_x', 320)
        self._size_y = _int(kwargs, 'size_y', 240)
        self._lock_ratio = _bool(kwargs'lock_ratio', True)

# Done !

Is this possible?
If yes, how?
How to implement the add_getter_setter_and_property() method?


Edit:
There is a pretty similar question here: Python Class Decorator
from the answers there I suspect that it is possible to achive something like I have asked, but can you give me a clue on how I could implement the add_getter_setter_and_property() function/method?


Note:
the _int() , _bool() functions just return a tkinter Int/Bool-var eighter from the kwargs if the string (fe 'size_x') exist or from the default value (fe 320).


My Final Solution: I think i have found a pretty good solution. I have to add a settings name only once, which imo is awesome :-)

import tkinter as tk

def _add_var_getter_property(cls, attr):
    """ this function is used in the settings_class decorator to add a
    getter for the tk-stringvar and a read/write property to the class.
    cls:  is the class where the attributes are added.
    attr: is the name of the property and for the get_XYZ_var() method.
    """
    field = '_' + attr
    setattr(cls, 'get_{}_var'.format(attr), lambda self: getattr(self, field))
    setattr(cls, attr,
            property(lambda self: getattr(self, field).get(),
                     lambda self, value: getattr(self, field).set(value)))

def settings_class(cls):
    """ this is the decorator function for SettingsBase subclasses.
    it adds getters for the tk-stringvars and properties. it reads the
    names described in the class-variable _SETTINGS.
    """
    for name in cls._SETTINGS:
        _add_var_getter_property(cls, name)
    return cls


class SettingsBase:
    """ this is the base class for a settings class. it automatically
    adds fields to the class described in the class variable _SETTINGS.
    when you subclass SettingsBase you should overwrite _SETTINGS.
    a minimal example could look like this:

      @settings_class
      class MySettings(SettingsBase):
          _SETTINGS = {
              'x': 42,
              'y': 23}

    this would result in a class with a _x tk-intvar and a _y tk-doublevar
    field with the getters get_x_var() and get_y_var() and the properties
    x and y.
    """

    _SETTINGS = {}

    def __init__(self, **kwargs):
        """ creates the fields described in _SETTINGS and initialize
        eighter from the kwargs or from the default values
        """
        super().__init__()
        fields = self._SETTINGS.copy()
        if kwargs:
            for key in kwargs:
                if key in fields:
                    typ = type(fields[key])
                    fields[key] = typ(kwargs[key])
                else:
                    raise KeyError(key)
        for key in fields:
            value = fields[key]
            typ = type(value)
            name = '_' + key
            if typ is int:
                var = tk.IntVar()
            elif typ is str:
                var = tk.StringVar()
            elif typ is bool:
                var = tk.BooleanVar()
            elif typ is float:
                var = tk.DoubleVar()
            else:
                raise TypeError(typ)
            var.set(value)
            setattr(self, name, var)

after that my settings classes simply look like this:

@settings_class
class _CanvasSettings(SettingsBase):

    _SETTINGS = {
        'size_x': 320,
        'size_y': 240,
        'lock_ratio': True
        }

Decorator for the class.

def add_get_set(cls):
    for prop in cls.PROPERTIES:
        # Note cannot be "lambda self: getattr(self, prop)" because of scope prop changes to be the last item in PROPERTIES
        setattr(cls, "get"+prop, lambda self, attr=prop: getattr(self, attr))

    return cls

@add_get_set
class _CanvasSettings:
    PROPERTIES = ["_size_x", "_size_y", "_lock_ratio"]

    def __init__(self, **kwargs):
        super().__init__()

        for prop in self.PROPERTIES:
            setattr(self, prop, 0)

c = _CanvasSettings()
print(c.get_size_y())

You could just set the functions as variables

class _CanvasSettings:
    def __init__(self, **kwargs):
        super().__init__()
        self._size_x = _int(kwargs, 'size_x', 320)
        self._size_y = _int(kwargs, 'size_y', 240)
        self._lock_ratio = _bool(kwargs'lock_ratio', True)

        for variable in ["_size_x", "_size_y", "_lock_ratio"]:
            setattr(self, "get"+variable, lambda: getattr(self, variable))
            # bind the method (Not sure if binding the method gets you anything)
            #setattr(self, "get"+variable, (lambda self: getattr(self, variable)).__get__(self, self.__class__))

Alternate

class _CanvasSettings:
    def __init__(self, **kwargs):
        super().__init__()
        self._size_x = _int(kwargs, 'size_x', 320)
        self._size_y = _int(kwargs, 'size_y', 240)
        self._lock_ratio = _bool(kwargs'lock_ratio', True)

        for variable in dir(self):
            if variable.startswith("_") and not variable.startswith("__"):
                self.__dict__["get"+variable] = lambda: getattr(self, variable)

It's certainly possible to do what you want, using setattr to bind the functions and property as attributes of the class object:

def add_getter_setter_property(cls, attrib_name):
    escaped_name = "_" + attrib_name
    setattr(cls, "get_{}_var".format(attrib_name),
            lambda self: getattr(self, escaped_name))
    setattr(cls, attrib_name,
            property(lambda self: getattr(self, escaped_name).get()
                     lambda self, value: getattr(self, escaped_name).set(value)))

Here I'm skipping giving names to the getter and setter methods used by the property . You could add them to the class if you really want to, but I think it's probably unnecessary.

The tricky bit may actually be finding which attribute names you need to apply this to. Unlike in your example, you can't iterate over a class object to get its attributes.

The easiest solution (from the implementation standpoint) would be to require the class to specify the names in a class variable:

def my_settings_class(cls):
    for field in cls._settings_vars:
        add_getter_setter_and_property(cls, field)
    return cls

@my_settings_class
class _CanvasSettings:
    _settings_vars = ["size_x", "size_y", "lock_ratio"]
    def __init__(self, **kwargs):
        super().__init__()
        self._size_x = _int(kwargs, 'size_x', 320)
        self._size_y = _int(kwargs, 'size_y', 240)
        self._lock_ratio = _bool(kwargs, 'lock_ratio', True)

A more user-friendly approach might use dir or vars to examine the classes variables and pick out the ones that need to be wrapped automatically. You could use isinstance to check if the value has a specific type, or look for a specific pattern in the attribute name. I don't know what is best for your specific use, so I'll leave this up to you.

As an alternative to automating making properties you could overload __getattr__ and __setattr__ to detect when a private field is available with an appropriate getter or setter method:

class Field: # so I could test it.
    def __init__(self,args,name,default):
        self.name = name
        self.value = default
    def get(self):
        return self.value
    def set(self,value):
        self.value = value

class CanvasSettings:
    def __init__(self, **kwargs):
        super().__init__()
        self._size_x = Field(kwargs, 'size_x', 320)
        self._size_y = Field(kwargs, 'size_y', 240)
        self._lock_ratio = Field(kwargs, 'lock_ratio', True)

    def __getattr__(self, attr):
        private_name = "_" + attr
        field = object.__getattribute__(self, private_name) #this will fail for non-special attributes
        getter = getattr(field,"get",None)
        if getter is None:
            raise AttributeError("Private member did not have getter") #may want to change the error handling
        else:
            return getter()

    def __setattr__(self,attr, value):
        private_name = "_" + attr
        try:
            field = getattr(self,private_name)
        except AttributeError:
            # if there is no private field or there is but no setter
            # resort back to defaualt behaviour.
            return super().__setattr__(attr,value)
        else:
            setter = getattr(field, "set", None)
            if setter is None:
                raise AttributeError("private field does not have a setter")
            setter(value)

Then when ever you try to get or set thing.size_x it will first look for a thing._size_x and check for an appropriate method, here is a demo:

>>> thing = CanvasSettings()
>>> thing._size_x.value
320
>>> thing.size_x
320
>>> thing.size_x = 5
>>> 5 == thing.size_x == thing._size_x.value
True

Checking for an existing field every time you retrieve the attribute may have penalties to performance but I just wanted to offer this as an alternative if you have many classes with private fields that fit this model.

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