【问题标题】:Python super and setting parent class property from subclassesPython super 和从子类设置父类属性
【发布时间】:2020-10-16 13:13:24
【问题描述】:

多年来,人们曾提出过类似的问题。 Python2 和 Python3 似乎工作方式相同。下面显示的代码可以正常工作并且可以理解(至少对我而言)。但是,有一个无操作让我感到困扰,我想知道是否有更优雅的方式来表达此功能。

关键问题是,当子类在超类中定义的变量中设置属性的新值时,超类应该将新修改的值保存到文件中。下面的代码这样做的方式是让每个子类在 setter 方法中都有这样的一行,这是一个无操作:

self.base_data_property.fset(self, super().data) 

我称之为无操作,因为在执行此行时超类的数据已经被修改,而该行代码存在的唯一原因是触发超类的@data.setter 方法的副作用,执行自动保存到文件。

我不喜欢编写这样的副作用代码。有没有更好的办法,除了显而易见的,那就是:

super().save_data()  # Called from each subclass setter

上面会调用而不是 no-op。

对下面代码的另一个批评是super()._base_data 显然不是子类lambda_data 的超集。这使得代码难以维护。这导致代码看起来有些神奇,因为更改lambda_data 中的属性实际上是更改super()._base_data 中的属性的别名。

代码

我为此代码创建了GitHub repo

import logging


class BaseConfig:

    def __init__(self, diktionary):
        self._base_data = diktionary
        logging.info(f"BaseConfig.__init__: set self.base_data = '{self._base_data}'")

    def save_data(self):
        logging.info(f"BaseConfig: Pretending to save self.base_data='{self._base_data}'")

    @property
    def data(self) -> dict:
        logging.info(f"BaseConfig: self.data getter returning = '{self._base_data}'")
        return self._base_data

    @data.setter
    def data(self, value):
        logging.info(f"BaseConfig: self.data setter, new value for self.base_data='{value}'")
        self._base_data = value
        self.save_data()


class LambdaConfig(BaseConfig):
    """ This example subclass is one of several imaginary subclasses, all with similar structures.
    Each subclass only works with data within a portion of super().data;
    for example, this subclass only looks at and modifies data within super().data['aws_lambda'].
    """

    def __init__(self, diktionary):
        super().__init__(diktionary)
        # See https://stackoverflow.com/a/10810545/553865:
        self.base_data_property = super(LambdaConfig, type(self)).data
        # This subclass only modifies data contained within self.lambda_data:
        self.lambda_data = super().data['aws_lambda']

    @property
    def lambda_data(self):
        return self.base_data_property.fget(self)['aws_lambda']

    @lambda_data.setter
    def lambda_data(self, new_value):
        super().data['aws_lambda'] = new_value
        self.base_data_property.fset(self, super().data)

    # Properties specific to this class follow

    @property
    def dir(self):
        result = self.data['dir']
        logging.info(f"LambdaConfig: Getting dir = '{result}'")
        return result

    @dir.setter
    def dir(self, new_value):
        logging.info(f"LambdaConfig: dir setter before setting to {new_value} is '{self.lambda_data['dir']}'")
        # Python's call by value means super().data is called, which modifies super().base_data:
        self.lambda_data['dir'] = new_value
        self.base_data_property.fset(self, super().data)  # This no-op merely triggers super().@data.setter
        logging.info(f"LambdaConfig.dir setter after set: self.lambda_data['dir'] = '{self.lambda_data['dir']}'")


    @property
    def name(self):  # Comments are as for the dir property
        return self.data['name']

    @name.setter
    def name(self, new_value):  # Comments are as for the dir property
        self.lambda_data['name'] = new_value
        self.base_data_property.fset(self, super().data)


    @property
    def id(self):  # Comments are as for the dir property
        return self.data['id']

    @id.setter
    def id(self, new_value):  # Comments are as for the dir property
        self.lambda_data['id'] = new_value
        self.base_data_property.fset(self, super().data)


if __name__ == "__main__":
    logging.basicConfig(
        format = '%(levelname)s %(message)s',
        level = logging.INFO
    )

    diktionary = {
        "aws_lambda": {
            "dir": "old_dir",
            "name": "old_name",
            "id": "old_id"
        },
        "more_keys": {
            "key1": "old_value1",
            "key2": "old_value2"
        }
    }

    logging.info("Superclass data can be changed from the subclass, new value appears everywhere:")
    logging.info("main: Creating a new LambdaConfig, which creates a new BaseConfig")
    lambda_config = LambdaConfig(diktionary)
    aws_lambda_data = lambda_config.data['aws_lambda']
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info("")

    lambda_config.dir = "new_dir"
    logging.info(f"main: after setting lambda_config.dir='new_dir', aws_lambda_data['dir'] = {aws_lambda_data['dir']}")
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info(f"main: aws_lambda_data['dir'] = '{aws_lambda_data['dir']}'")
    logging.info("")

    lambda_config.name = "new_name"
    logging.info(f"main: after setting lambda_config.name='new_name', aws_lambda_data['name'] = {aws_lambda_data['name']}")
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info(f"main: aws_lambda_data['name'] = '{aws_lambda_data['name']}'")

    lambda_config.id = "new_id"
    logging.info(f"main: after setting lambda_config.id='new_id', aws_lambda_data['id'] = {aws_lambda_data['id']}")
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info(f"main: aws_lambda_data['id'] = '{aws_lambda_data['id']}'")

输出

INFO Superclass data can be changed from the subclass, new value appears everywhere:
INFO main: Creating a new LambdaConfig, which creates a new BaseConfig
INFO BaseConfig.__init__: set self.base_data = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data setter, new value for self.base_data='{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: Pretending to save self.base_data='{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO main: aws_lambda_data = {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}
INFO 
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO LambdaConfig: dir setter before setting to new_dir is 'old_dir'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'old_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'new_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data setter, new value for self.base_data='{'aws_lambda': {'dir': 'new_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: Pretending to save self.base_data='{'aws_lambda': {'dir': 'new_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'new_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO LambdaConfig.dir setter after set: self.lambda_data['dir'] = 'new_dir'
INFO main: after setting lambda_config.dir='new_dir', aws_lambda_data['dir'] = new_dir
INFO main: aws_lambda_data = {'dir': 'new_dir', 'name': 'old_name', 'id': 'old_id'}
INFO main: aws_lambda_data['dir'] = 'new_dir'
INFO 
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'new_dir', 'name': 'old_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data setter, new value for self.base_data='{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: Pretending to save self.base_data='{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO main: after setting lambda_config.name='new_name', aws_lambda_data['name'] = new_name
INFO main: aws_lambda_data = {'dir': 'new_dir', 'name': 'new_name', 'id': 'old_id'}
INFO main: aws_lambda_data['name'] = 'new_name'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'old_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data getter returning = '{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'new_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: self.data setter, new value for self.base_data='{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'new_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO BaseConfig: Pretending to save self.base_data='{'aws_lambda': {'dir': 'new_dir', 'name': 'new_name', 'id': 'new_id'}, 'more_keys': {'key1': 'old_value1', 'key2': 'old_value2'}}'
INFO main: after setting lambda_config.id='new_id', aws_lambda_data['id'] = new_id
INFO main: aws_lambda_data = {'dir': 'new_dir', 'name': 'new_name', 'id': 'new_id'}
INFO main: aws_lambda_data['id'] = 'new_id'

【问题讨论】:

  • 我还没有找到一个继承property 的好方法(尽管我没有非常努力地尝试过)。一种替代方法是定义您自己的类似属性的描述符来代替property

标签: python


【解决方案1】:

.fset 不会自动触发这一事实与在超类中定义的属性无关。如果在子类上设置self.data,那么setter会按预期无缝触发。

问题是你没有设置self.data。该属性引用一个可变对象,并且您正在该对象中进行更改(设置新键)。像super().data['aws_lambda'] = new_value 这样的行中的所有属性机制是对super().data 的读取访问 - Python 解析表达式的这一部分并返回一个字典 - 然后你设置字典键。

(顺便说一句,如果您不将 data 重新定义为子类中的属性,那么 super() 调用在那里是多余的 - 如果无论如何设置了这样的覆盖属性,可能不会做您想做的事情 - 您可以(并且很可能应该)在这些访问中使用self.data)。

无论如何,你不是唯一一个遇到这个问题的人——SQLAlchemy ORM 也遇到了这个问题,并且不遗余力地提供方法来指示检测属性中的字典(或其他可变值)是“脏”的,以便将其刷新到数据库中。

您有两个选择:(1)显式触发数据保存,这就是您正在执行的操作; (2) 使用一个专门的派生字典类,该类知道它应该在更改时触发保存。

(2) 中的方法很优雅,但需要做大量工作才能正确完成 - 您至少需要一个 Mapping 和一个 Sequence 专用类来实现这些模式,以支持嵌套属性。不过,如果操作正确,它将可靠地工作。

由于您已经将字典值封装在类属性中(因此执行 (1)),并且它可以无缝地为您的类用户工作,我想说您可以保持这种方式。您可能需要一个显式的 _ 前缀方法来强制将参数保存在超类中,而不是手动触发属性设置器,但就是这样。

啊,是的,就像我上面说的:

    @lambda_data.setter
    def lambda_data(self, new_value):
        data = self.data
        data['aws_lambda'] = new_value
        # Trigger property setter, which performs the "save":
        self.data = data

不需要所有那些super() 调用和setattr。

如果您对 Python“镜头”(cmets 中的链接)感到满意,它可用于在一行中编写您的设置器 - 因为它既设置新值又返回变异的值对象。

您可能会在并发代码中遇到问题 - 如果一段代码正在保存和更改 self.data,并且它被镜头返回的全新对象替换:

from lenses import lens

    @lambda_data.setter
    def lambda_data(self, new_value):
        self.data = lens['aws_lambda'].set(new_value)(self.data)

    ...
    @dir.setter
    def lambda_data(self, new_value):
        self.lambda_data = lens['dir'].set(new_value)(self.lambda_data)

(使这项工作有效的原因是我们实际上是在调用每个属性的设置器,并使用镜头调用创建的新对象)

【讨论】:

  • 感谢您的快速回复。如果我删除那些super() 电话,我会得到NameError: name 'data' is not defined。另外,我想知道镜头是否有帮助(python-lenses.readthedocs.io/en/latest/tutorial/intro.html)。
  • 您的lambda_data 方法是一项重大改进。
  • 你会如何在LambdaConfig 中写@dir.setter
  • 它可以像 lambda_data.setter 一样完成。另外,在您的第一条评论中,我看不出self.data 会如何引发 NameError - (只是“数据”是一个局部变量,必须先定义它)
  • lens 可以将您的设置器减少到一行。如果您对示例的答案感到满意
【解决方案2】:

我使用@jsbueno 描述的方法重新编写了我的原始代码并对其进行了优化:

import logging


class BaseConfig:

    def __init__(self, _dictionary):
        self._base_data = _dictionary
        logging.info(f"BaseConfig.__init__: set self.base_data = '{self._base_data}'")

    def save_data(self):
        logging.info(f"BaseConfig: Pretending to save self.base_data='{self._base_data}'")

    @property
    def data(self) -> dict:
        logging.info(f"BaseConfig: self.data getter returning = '{self._base_data}'")
        return self._base_data

    @data.setter
    def data(self, value):
        logging.info(f"BaseConfig: self.data setter, new value for self.base_data='{value}'")
        self._base_data = value
        self.save_data()


class LambdaConfig(BaseConfig):
    """ This example subclass is one of several imaginary subclasses, all with similar structures.
    Each subclass only works with data within a portion of super().data;
    for example, this subclass only looks at and modifies data within super().data['aws_lambda'].
    """

    # Start of boilerplate; each BaseConfig subclass needs something like the following:

    def __init__(self, _dictionary):
        super().__init__(_dictionary)

    @property
    def lambda_data(self):
        return self.data['aws_lambda']

    @lambda_data.setter
    def lambda_data(self, new_value):
        data = self.data
        data['aws_lambda'] = new_value
        self.data = data  # Trigger the super() data.setter, which saves to a file

    def generalized_setter(self, key, new_value):
        lambda_data = self.lambda_data
        lambda_data[key] = new_value
        # Python's call by value means the super().data setter is called, which modifies super().base_data:
        self.lambda_data = lambda_data

    # End of boilerplate. Properties specific to this class follow:

    @property
    def dir(self):
        return self.data['dir']

    @dir.setter
    def dir(self, new_value):
        self.generalized_setter("dir", new_value)


    @property
    def name(self):
        return self.data['name']

    @name.setter
    def name(self, new_value):
        self.generalized_setter("name", new_value)


    @property
    def id(self):
        return self.data['id']

    @id.setter
    def id(self, new_value):
        self.generalized_setter("id", new_value)


if __name__ == "__main__":
    logging.basicConfig(
        format = '%(levelname)s %(message)s',
        level = logging.INFO
    )

    diktionary = {
        "aws_lambda": {
            "dir": "old_dir",
            "name": "old_name",
            "id": "old_id"
        },
        "more_keys": {
            "key1": "old_value1",
            "key2": "old_value2"
        }
    }

    logging.info("Superclass data can be changed from the subclass, new value appears everywhere:")
    logging.info("main: Creating a new LambdaConfig, which creates a new BaseConfig")
    lambda_config = LambdaConfig(diktionary)
    aws_lambda_data = lambda_config.data['aws_lambda']
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info("")

    lambda_config.dir = "new_dir"
    logging.info(f"main: after setting lambda_config.dir='new_dir', aws_lambda_data['dir'] = {aws_lambda_data['dir']}")
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info(f"main: aws_lambda_data['dir'] = '{aws_lambda_data['dir']}'")
    logging.info("")

    lambda_config.name = "new_name"
    logging.info(f"main: after setting lambda_config.name='new_name', aws_lambda_data['name'] = {aws_lambda_data['name']}")
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info(f"main: aws_lambda_data['name'] = '{aws_lambda_data['name']}'")
    logging.info("")

    lambda_config.id = "new_id"
    logging.info(f"main: after setting lambda_config.id='new_id', aws_lambda_data['id'] = {aws_lambda_data['id']}")
    logging.info(f"main: aws_lambda_data = {aws_lambda_data}")
    logging.info(f"main: aws_lambda_data['id'] = '{aws_lambda_data['id']}'")
    logging.info("")

    logging.info(f"main: lambda_config.data = {lambda_config.data}")

【讨论】:

    猜你喜欢
    • 2012-06-04
    • 2017-06-17
    • 2011-06-16
    • 1970-01-01
    • 1970-01-01
    • 2018-07-25
    • 1970-01-01
    • 1970-01-01
    • 2013-12-24
    相关资源
    最近更新 更多