簡體   English   中英

Python super 和從子類設置父類屬性

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

多年來,類似的問題也曾被問過。 Python2 和 Python3 似乎工作相同。 下面顯示的代碼工作正常並且可以理解(至少對我而言)。 但是,有一個空操作困擾着我,我想知道是否可能有一種更優雅的方式來表達此功能。

關鍵問題是,當子類在超類中定義的變量中設置屬性的新值時,超類應該將新修改的值保存到文件中。 下面的代碼執行此操作的方式是讓每個子類在 setter 方法中都有這樣一行,這是一個無操作:

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

我將其稱為無操作,因為在執行此行時超類的數據已被修改,並且該行代碼存在的唯一原因是觸發超類的@data.setter方法的@data.setter ,它執行自動保存到文件。

我不喜歡寫這樣的副作用代碼。 除了顯而易見的方法之外,還有更好的方法是:

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

以上將調用而不是無操作。

下面代碼的另一個批評是super()._base_data顯然不是子類lambda_data的超集。 這使得代碼難以維護。 這導致代碼看起來有些神奇,因為更改lambda_data的屬性實際上是更改super()._base_data的屬性的別名。

代碼

我為此代碼制作了一個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']}'")

輸出

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'

.fset不會自動觸發的事實與在超類中定義的屬性無關。 如果您在子類上設置self.data ,setter 將按預期無縫觸發。

問題是您沒有設置self.data 該屬性指的是一個可變對象,您正在該對象中進行更改(設置新鍵)。 所有屬性機制都在一行中,如super().data['aws_lambda'] = new_value是對super().data的讀取訪問 - Python 解析表達式的那部分並返回一個字典 - 然后你設置字典鍵.

(順便說一句, super() 調用在那里是多余的,如果您不將data重新定義為子類中的屬性 - 如果無論如何設置了這樣的覆蓋屬性,則可能不會做您想做的事情 - 您可以(並且可能應該) 只是在這些訪問中使用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 “lens”(注釋中的鏈接)感到滿意,則可以使用它在一行中編寫您的 setter - 因為它既設置新值又返回變異對象。

您可能會在並發代碼中遇到問題 - 如果一段代碼正在保存和更改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)

(使這項工作起作用的是,我們實際上是為每個屬性調用 setter,使用由鏡頭調用創建的新對象)

我使用@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