【问题标题】:Is it possible to modify a class with a decorator是否可以使用装饰器修改类
【发布时间】:2016-10-09 05:00:18
【问题描述】:

我正在 python 中为一些设置编写一个类,如下所示:

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)

如你所见,我添加了块:

    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)

适用于每一个设置。
是否可以使用decorator 自动执行此任务?

我想这样做(伪代码):

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 !

这可能吗?
如果是,怎么做?
add_getter_setter_and_property()方法如何实现?


编辑:
这里有一个非常相似的问题:Python Class Decorator
从那里的答案中,我怀疑有可能实现我所问的事情,但是你能给我一个关于如何实现add_getter_setter_and_property() 函数/方法的线索吗?


注意:
_int()_bool() 函数只是从 kwargs 中返回一个 tkinter Int/Bool-var 八进制,如果字符串(例如 'size_x')存在或从默认值(例如 320)。


我的最终解决方案: 我想我找到了一个很好的解决方案。我只需要添加一次设置名称,这太棒了:-)

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)

之后我的设置类看起来像这样:

@settings_class
class _CanvasSettings(SettingsBase):

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

【问题讨论】:

  • Python Class Decorator的可能重复
  • @ppperry 我的问题部分非常相似(感谢链接!),但是您(或其他人)能否给我一个线索,我该如何实现add_gett_setter_and_property 方法/功能?
  • 使用dir函数。

标签: python class python-3.x decorator


【解决方案1】:

类的装饰器。

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())

您可以将函数设置为变量

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__))

替代

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)

【讨论】:

  • 虽然可以通过这种方式将“预绑定”函数添加为实例变量,但您不能对 property 实例执行相同操作。由于它们使用描述符协议,因此确实需要将它们分配给类,而不是实例。
【解决方案2】:

当然可以为所欲为,使用setattr将函数和属性绑定为类对象的属性:

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)))

这里我将跳过给property 使用的gettersetter 方法命名。如果你真的想要,你可以将它们添加到课程中,但我认为这可能是不必要的。

棘手的一点实际上可能是找到您需要将其应用于哪些属性名称。与您的示例不同,您不能遍历类对象来获取其属性。

最简单的解决方案(从实现的角度来看)是要求类在类变量中指定名称:

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)

更用户友好的方法可能使用dirvars 来检查类变量并挑选出需要自动包装的变量。您可以使用isinstance 来检查值是否具有特定类型,或者在属性名称中查找特定模式。我不知道什么最适合你的具体用途,所以我会留给你。

【讨论】:

  • 谢谢!它对我有用。我只需要将lambda self: return self.getattr(escaped_name) 更改为lambda self: getattr(self, escaped_name)。如果你能为下一个阅读这篇文章的人修复它,那就太好了。
  • 啊,这是我犯的一个相当愚蠢的错误。我已经编辑修复它。
【解决方案3】:

作为自动生成属性的替代方法,您可以重载 __getattr____setattr__ 以使用适当的 getter 或 setter 方法检测私有字段何时可用:

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)

然后,当您尝试获取或设置thing.size_x 时,它会首先查找thing._size_x 并检查适当的方法,这里有一个演示:

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

每次检索属性时检查现有字段可能会降低性能,但如果您有许多具有适合此模型的私有字段的类,我只想提供此选项。

【讨论】:

    猜你喜欢
    • 2018-08-31
    • 2014-03-18
    • 2023-03-15
    • 2013-02-22
    • 2017-02-04
    • 2012-09-03
    • 1970-01-01
    • 1970-01-01
    • 2016-10-06
    相关资源
    最近更新 更多