简体   繁体   English

Python super 和从子类设置父类属性

[英]Python super and setting parent class property from subclasses

Similar questions has been asked before over the years.多年来,类似的问题也曾被问过。 Python2 and Python3 seem to work the same. Python2 和 Python3 似乎工作相同。 The code shown below works fine and is understandable (at least, to me).下面显示的代码工作正常并且可以理解(至少对我而言)。 However, there is a no-op that bothers me and I wonder if there might be a more elegant way of expressing this functionality.但是,有一个空操作困扰着我,我想知道是否可能有一种更优雅的方式来表达此功能。

The key issue is that when a subclass sets a new value of a property in a variable defined in the superclass, the superclass should save the newly modified value to a file.关键问题是,当子类在超类中定义的变量中设置属性的新值时,超类应该将新修改的值保存到文件中。 The way the code below does this is for each subclass to have a line like this, which is a no-op, in a setter method:下面的代码执行此操作的方式是让每个子类在 setter 方法中都有这样一行,这是一个无操作:

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

I call this a no-op because the superclass's data has already been modified by the time this line is executed, and the only reason for that line of code to exist is for the side effect of triggering the superclass's @data.setter method, which performs the automatic saving to a file.我将其称为无操作,因为在执行此行时超类的数据已被修改,并且该行代码存在的唯一原因是触发超类的@data.setter方法的@data.setter ,它执行自动保存到文件。

I don't like writing side-effecty code like this.我不喜欢写这样的副作用代码。 Is there a better way, other than the obvious, which is:除了显而易见的方法之外,还有更好的方法是:

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

The above would called instead of the no-op.以上将调用而不是无操作。

Another critizism of the code below is that the super()._base_data is not obviously a superset of the subclass lambda_data .下面代码的另一个批评是super()._base_data显然不是子类lambda_data的超集。 This makes the code hard to maintain.这使得代码难以维护。 This results in the code seems to be somewhat magical because changing a property in lambda_data is actually aliases to changing a property in super()._base_data .这导致代码看起来有些神奇,因为更改lambda_data的属性实际上是更改super()._base_data的属性的别名。

Code代码

I made a GitHub repo for this code.我为此代码制作了一个GitHub 存储库

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']}'")

Output输出

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'

The fact that .fset is not triggered automatically has nothing to do with the property being defined in a superclass. .fset不会自动触发的事实与在超类中定义的属性无关。 If you set self.data on the subclass, the setter will be triggered seamlessly as expected.如果您在子类上设置self.data ,setter 将按预期无缝触发。

The problem is that you are not setting self.data .问题是您没有设置self.data The property refers to a mutable object, and you are making changes (setting new keys) in that object.该属性指的是一个可变对象,您正在该对象中进行更改(设置新键)。 All the property mechanism get in a line like super().data['aws_lambda'] = new_value is a read access to super().data - Python resolves that part of the expression and returns a dictionary - then you set the dictionary key.所有属性机制都在一行中,如super().data['aws_lambda'] = new_value是对super().data的读取访问 - Python 解析表达式的那部分并返回一个字典 - 然后你设置字典键.

(BTW, the super() call is redundant there, if you don't redefine data as a property in the subclass - and likely not doing what you'd want if such an overider property were set anyway - you could (and likely should) just be using self.data in these accesses). (顺便说一句, super() 调用在那里是多余的,如果您不将data重新定义为子类中的属性 - 如果无论如何设置了这样的覆盖属性,则可能不会做您想做的事情 - 您可以(并且可能应该) 只是在这些访问中使用self.data )。

Anyway, you are not the only person with this problem - the SQLAlchemy ORM also suffers from this and goes out of its way to provide ways to signal a dictionary (or other mutable value) in an instrumented attribute is "dirty", so that it is flushed to the database.无论如何,您不是唯一遇到此问题的人 - SQLAlchemy ORM 也受此困扰,并竭尽全力提供在检测属性中发出信号字典(或其他可变值)是“脏的”的方法,以便它被刷新到数据库。

You are left with two options: (1) explicitly triggering the data save, which is what you are doing;你有两个选择:(1)显式触发数据保存,这就是你正在做的事情; (2) Use a specialized derived dictionary class that is aware it should trigger the saving when it is changed. (2) 使用一个专门的派生字典类,它知道它应该在更改时触发保存。

The approach in (2) is elegant, but takes a lot of work to be done correctly - you'd need at least a Mapping and a Sequence specialized classes implementing these patterns in order to support nested attributes. (2) 中的方法很优雅,但需要大量工作才能正确完成 - 您至少需要一个 Mapping 和一个 Sequence 专用类来实现这些模式以支持嵌套属性。 It will work reliably if done correctly, though.但是,如果操作正确,它将可靠地工作。

Since you are encapsulating the dictionary values in class attributes already, (thus doing (1)), and it works seamlessly for your class' users, I'd say you could keep it that way.由于您已经将字典值封装在类属性中(因此执行 (1)),并且它可以无缝地为您的类用户工作,我想说您可以保持这种方式。 You might want an explicit, _ prefixed method to force saving the parameter in the super class, instead of manually triggering the property setter, but that is it.您可能需要一个显式的_前缀方法来强制将参数保存在超类中,而不是手动触发属性设置器,但就是这样。

Ah, yes, and like I said above:啊,是的,就像我上面说的:

    @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

No need for all those super() calls, and setattr.不需要所有这些super()调用和 setattr。

if you feel confortable with Python "lens" (link in the comments), it can be used to write your setters in a single line - since it both sets the new value and returns the mutated object.如果您对 Python “lens”(注释中的链接)感到满意,则可以使用它在一行中编写您的 setter - 因为它既设置新值又返回变异对象。

You can have problems with that in concurrent code - if one piece of code is holding and changing self.data , and it is replaced by a whole new object returned by the lens:您可能会在并发代码中遇到问题 - 如果一段代码正在保存和更改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)

(what makes this work is that we are actually calling the setters for each property, with the new object created by the lens call) (使这项工作起作用的是,我们实际上是为每个属性调用 setter,使用由镜头调用创建的新对象)

I redid my original code using the approach described by @jsbueno and optimized it:我使用@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}")

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM