【问题标题】:Python: Idiomatic properties for structured data?Python:结构化数据的惯用属性?
【发布时间】:2012-05-31 13:59:42
【问题描述】:

我的代码有异味。也许我只是需要让它通风一下,但现在它让我很烦。

我需要创建三个不同的输入文件来运行三个辐射传递模型 (RTM) 应用程序,以便比较它们的输出。这个过程将针对数千组输入重复,所以我使用 python 脚本将其自动化。

我想将输入参数存储为一个通用 python 对象,我可以将它传递给其他三个函数,每个函数都会将该通用对象转换为运行他们负责的 RTM 软件所需的特定参数。我认为这是有道理的,但请随意批评我的方法。

每个 RTM 软件都有许多可能的输入参数。他们中的许多人重叠。它们中的大多数都保持合理的默认值,但应该很容易更改。

我从一个简单的dict开始

config = {
    day_of_year: 138,
    time_of_day: 36000, #seconds
    solar_azimuth_angle: 73, #degrees
    solar_zenith_angle: 17, #degrees
    ...
}

参数很多,可以很干净的归类,所以想到了dict里面的dicts:

config = {
    day_of_year: 138,
    time_of_day: 36000, #seconds
    solar: {
        azimuth_angle: 73, #degrees
        zenith_angle: 17, #degrees
        ...
    },
    ...
}

我喜欢这样。但是有很多多余的属性。例如,如果知道另一个方位角和天顶角,就可以找到太阳方位角和天顶角,那么为什么要对两者都进行硬编码呢?所以我开始研究python的内置property。如果我将数据存储为对象属性,这让我可以对数据做一些漂亮的事情:

class Configuration(object):
    day_of_year = 138,
    time_of_day = 36000, #seconds
    solar_azimuth_angle = 73, #degrees
    @property
    def solar_zenith_angle(self):
        return 90 - self.solar_azimuth_angle
    ...

config = Configuration()

但现在我失去了第二个 dict 示例中的结构。

请注意,有些属性没有我的solar_zenith_angle 示例那么简单,并且可能需要访问它所属的属性组之外的其他属性。例如,如果我知道一年中的哪一天、一天中的时间、纬度和经度,我可以计算出solar_azimuth_angle

我在寻找什么:

一种存储配置数据的简单方法,其值都可以以统一的方式访问,结构良好,可以作为属性(实际值)或属性(从其他属性计算)存在。

有点无聊的可能性:

将所有内容存储在我之前概述的 dicts 中,并让其他函数在对象上运行并计算可计算值?这听起来不好玩。或干净。对我来说,这听起来很混乱和令人沮丧。

一个丑陋的作品:

经过很长时间尝试不同的策略,但大多无处可去,我想出了一个似乎可行的解决方案:

我的课:(闻起来有点古怪,呃,时髦。绝对是。)

class SubConfig(object):
    """
    Store logical groupings of object attributes and properties.

    The parent object must be passed to the constructor so that we can still
    access the parent object's other attributes and properties. Useful if we
    want to use them to compute a property in here.
    """
    def __init__(self, parent, *args, **kwargs):
        super(SubConfig, self).__init__(*args, **kwargs)
        self.parent = parent


class Configuration(object):
    """
    Some object which holds many attributes and properties.

    Related configurations settings are grouped in SubConfig objects.
    """
    def __init__(self, *args, **kwargs):
        super(Configuration, self).__init__(*args, **kwargs)
        self.root_config = 2

        class _AConfigGroup(SubConfig):
            sub_config = 3
            @property
            def sub_property(self):
                return self.sub_config * self.parent.root_config
        self.group = _AConfigGroup(self) # Stinky?!

如何使用它们:(随心所欲)

config = Configuration()

# Inspect the state of the attributes and properties.
print("\nInitial configuration state:")
print("config.rootconfig: %s" % config.root_config)
print("config.group.sub_config: %s" % config.group.sub_config)
print("config.group.sub_property: %s (calculated)" % config.group.sub_property)

# Inspect whether the properties compute the correct value after we alter
# some attributes.
config.root_config = 4
config.group.sub_config = 5

print("\nState after modifications:")
print("config.rootconfig: %s" % config.root_config)
print("config.group.sub_config: %s" % config.group.sub_config)
print("config.group.sub_property: %s (calculated)" % config.group.sub_property)

行为:(上述所有代码的执行输出,符合预期)

Initial configuration state:
config.rootconfig: 2
config.group.sub_config: 3
config.group.sub_property: 6 (calculated)

State after modifications:
config.rootconfig: 4
config.group.sub_config: 5
config.group.sub_property: 20 (calculated)

为什么我不喜欢它:

在主对象的__init__() 内的类定义中存储配置数据并不优雅。特别是必须在像这样定义之后立即实例化它们。啊。当然,我可以为父类处理这个问题,但是在构造函数中进行......

在主 Configuration 对象之外存储相同的类也不优雅,因为内部类中的属性可能取决于 Configuration 的属性(或它们在其中的兄弟姐妹)。

我可以处理定义一切之外的功能,所以在里面有类似的东西

@property
def solar_zenith_angle(self):
   return calculate_zenith(self.solar_azimuth_angle)

但我不知道该怎么做

@property
def solar.zenith_angle(self):
    return calculate_zenith(self.solar.azimuth_angle)

(当我试图变得聪明时,我总是遇到<property object at 0xXXXXX>

那么正确的方法是什么?我错过了一些基本的东西还是采取了非常错误的方法?有谁知道一个聪明的解决方案?

帮助!我的python代码不漂亮!我一定是做错了什么!

【问题讨论】:

    标签: python object properties inner-classes


    【解决方案1】:

    菲尔,

    你对 func-y config 的犹豫我很熟悉 :)

    我建议您不要将配置存储为 python 文件,而是存储为结构化数据文件。我个人更喜欢 YAML,因为它看起来很干净,就像你一开始设计的那样。当然,您需要为自动计算的属性提供公式,但除非您输入太多代码,否则这还不错。这是我使用 PyYAML 库的实现。

    配置文件(config.yml):

    day_of_year: 138
    time_of_day: 36000 # seconds
    solar:
      azimuth_angle: 73 # degrees
      zenith_angle: !property 90 - self.azimuth_angle
    

    代码:

    import yaml
    
    yaml.add_constructor("tag:yaml.org,2002:map", lambda loader, node:
        type("Config", (object,), loader.construct_mapping(node))())
    
    yaml.add_constructor("!property", lambda loader, node:
        property(eval("lambda self: " + loader.construct_scalar(node))))
    
    config = yaml.load(open("config.yml"))
    
    print "LOADED config.yml"
    print "config.day_of_year:", config.day_of_year
    print "config.time_of_day:", config.time_of_day
    print "config.solar.azimuth_angle:", config.solar.azimuth_angle
    print "config.solar.zenith_angle:", config.solar.zenith_angle, "(calculated)"
    print
    
    config.solar.azimuth_angle = 65
    print "CHANGED config.solar.azimuth_angle = 65"
    print "config.solar.zenith_angle:", config.solar.zenith_angle, "(calculated)"
    

    输出:

    LOADED config.yml
    config.day_of_year: 138
    config.time_of_day: 36000
    config.solar.azimuth_angle: 73
    config.solar.zenith_angle: 17 (calculated)
    
    CHANGED config.solar.azimuth_angle = 65
    config.solar.zenith_angle: 25 (calculated)
    

    配置可以是任何深度,属性可以使用任何子组值。试试这个例子:

    a: 1
    b:
      c: 3
      d: some text
      e: true
      f:
        g: 7.01
    x: !property self.a + self.b.c + self.b.f.g
    

    假设你已经加载了这个配置:

    >>> config
    <__main__.Config object at 0xbd0d50>
    >>> config.a
    1
    >>> config.b
    <__main__.Config object at 0xbd3bd0>
    >>> config.b.c
    3
    >>> config.b.d
    'some text'
    >>> config.b.e
    True
    >>> config.b.f
    <__main__.Config object at 0xbd3c90>
    >>> config.b.f.g
    7.01
    >>> config.x
    11.01
    >>> config.b.f.g = 1000
    >>> config.x
    1004
    

    更新

    让我们有一个属性 config.b.x,它在其公式中同时使用自身、父级和子组属性:

    a: 1
    b:
      x: !property self.parent.a + self.c + self.d.e
      c: 3
      d:
        e: 5
    

    然后我们只需要在子组中添加对父级的引用:

    import yaml
    
    def construct_config(loader, node):
        attrs = loader.construct_mapping(node)
        config = type("Config", (object,), attrs)()
        for k, v in attrs.iteritems():
            if v.__class__.__name__ == "Config":
                setattr(v, "parent", config)
        return config
    
    yaml.add_constructor("tag:yaml.org,2002:map", construct_config)
    
    yaml.add_constructor("!property", lambda loader, node:
        property(eval("lambda self: " + loader.construct_scalar(node))))
    
    config = yaml.load(open("config.yml"))
    

    让我们看看它是如何工作的:

    >>> config.a
    1
    >>> config.b.c
    3
    >>> config.b.d.e
    5
    >>> config.b.parent == config
    True
    >>> config.b.d.parent == config.b
    True
    >>> config.b.x
    9
    >>> config.a = 1000
    >>> config.b.x
    1008
    

    【讨论】:

    • 将配置数据保存在一个单独的、人类可读的文件中似乎是一种很好的方法。我对将 python 嵌入 YAML 的可能性很感兴趣,我必须做一些实验!
    • 看起来很有希望,但我不知道如何从节点内访问属于节点父级的变量。例如,如果在上一个示例中,您必须添加ac 才能获得d...d: !property self.parent.a + self.c'. I wouldn't mind always referencing from the root, something like 'd: !property self.a + self.b.d。我想我得阅读更多关于yaml.add_constructor()的信息。
    • @Phil:增加了对 self.parent 的支持
    【解决方案2】:

    好吧,这是一种至少确保您的属性被调用的丑陋方法:

    class ConfigGroup(object):
        def __init__(self, config):
            self.config = config
    
        def __getattribute__(self, name):
            v = object.__getattribute__(self, name)
            if hasattr(v, '__get__'):
                return v.__get__(self, ConfigGroup)
            return v
    
    class Config(object):
        def __init__(self):
            self.a = 10
            self.group = ConfigGroup(self)
            self.group.a = property(lambda group: group.config.a*2)
    

    当然,此时您不妨完全放弃property,只检查该属性是否可在__getattribute__ 中调用。

    或者你可以全力以赴,享受元类的乐趣:

    def config_meta(classname, parents, attrs):
        defaults = {}
        groups = {}
        newattrs = {'defaults':defaults, 'groups':groups}
        for name, value in attrs.items():
            if name.startswith('__'):
                newattrs[name] = value
            elif isinstance(value, type):
                groups[name] = value
            else:
                defaults[name] = value
        def init(self):
            for name, value in defaults.items():
                self.__dict__[name] = value
            for name, value in groups.items():
                group = value()
                group.config = self
                self.__dict__[name] = group
        newattrs['__init__'] = init
        return type(classname, parents, newattrs)
    
    class Config2(object):
        __metaclass__ = config_meta
        a = 10
        b = 2
        class group(object):
            c = 5
            @property
            def d(self):
                return self.c * self.config.a
    

    像这样使用它:

    >>> c2.a
    10
    >>> c2.group.d
    50
    >>> c2.a = 6
    >>> c2.group.d
    30
    

    最终编辑 (?):如果您不想在子组属性定义中使用 self.config“回溯”,则可以改用以下内容:

    class group_property(property):
        def __get__(self, obj, objtype=None):
            return super(group_property, self).__get__(obj.config, objtype)
    
        def __set__(self, obj, value):
            super(group_property, self).__set__(obj.config, value)
    
        def __delete__(self, obj):
            return super(group_property, self).__del__(obj.config)
    
    class Config2(object):
        ...
        class group(object):
            ...
            @group_property
            def e(config):
                return config.group.c * config.a
    

    group_property 接收基本配置对象而不是组对象,因此路径始终从根开始。因此e等价于之前定义的d

    顺便说一句,支持嵌套组留给读者作为练习。

    【讨论】:

    • 我有种感觉,我可能不得不深入研究其中一些神奇的方法。我将稍微考虑一下您的代码建议,但乍一看我担心property(lambda...。我的一些属性计算不适合lambda,这就是我开始绕圈子尝试做def self.group.a(): 的地方。虽然我现在有一些新想法可以玩,但谢谢!
    • @Phil 然后尝试元类方法。它不需要你使用 lambda。
    【解决方案3】:

    哇,我今天刚读到一篇关于 r/python 描述符的文章,但我认为破解描述符不会给你想要的。

    我所知道的唯一能处理此类子配置的是flatland。无论如何,这就是它在 Flatland 中的工作方式。

    但你可以这样做:

    class Configuration(Form):
        day_of_year = Integer
        time_of_day = Integer
    
        class solar(Form):
            azimuth_angle = Integer
            solar_angle = Integer
    

    然后把字典加载进去

    config = Configuration({
        day_of_year: 138,
        time_of_day: 36000, #seconds
        solar: {
            azimuth_angle: 73, #degrees
            zenith_angle: 17, #degrees
            ...
        },
        ...
    })
    

    我喜欢平原,但我不确定你使用它会获得多少收益。

    您可以在类定义中添加元类或装饰器。

    类似

    def instantiate(klass):
         return klass()
    
    class Configuration(object):
         @instantiate
         class solar(object):
             @property
             def azimuth_angle(self):
                 return self.azimuth_angle
    

    这可能会更好。然后在 Configuration 上创建一个漂亮的__init__,它可以从字典中加载所有数据。我不知道也许其他人有更好的主意。

    这里有一些更完整的东西(没有 LaC 的答案那么神奇,但没有那么通用)。

    def instantiate(clazz): return clazz()
    
    #dummy functions for testing
    calc_zenith_angle = calc_azimuth_angle = lambda(x): 3
    
    class Solar(object):
        def __init__(self):
            if getattr(self,'azimuth_angle',None) is None and getattr(self,'zenith_angle',None) is None:
                return AttributeError("must have either azimuth_angle or zenith_angle provided")
    
            if getattr(self,'zenith_angle',None) is None:
                self.zenith_angle = calc_zenith_angle(self.azimuth_angle)
    
            elif getattr(self,'azimuth_angle',None) is None:
                self.azimuth_angle = calc_azimuth_angle(self.zenith_angle)
    
    class Configuration(object):
        day_of_year = 138
        time_of_day = 3600
        @instantiate
        class solar(Solar):
            azimuth_angle = 73
            #zenith_angle = 17 #not defined
    
    #if you don't want auto-calculation to be done automagically
    class ConfigurationNoAuto(object):
        day_of_year = 138
        time_of_day = 3600
        @instantiate
        class solar(Solar):
            azimuth_angle = 73
    
            @property
            def zenith_angle(self):
                return calc_zenith_angle(self.azimuth_angle)
    
    config = Configuration()
    config_no_auto = ConfigurationNoAuto()
    
    >>> config.day_of_year
    138
    >>> config_no_auto.day_of_year
    138
    >>> config_no_auto.solar.azimuth_angle
    73
    >>> config_no_auto.solar.zenith_angle
    3
    >>> config.solar.zenith_angle
    3
    >>> config.solar.azimuth_angle
    7
    

    【讨论】:

    • 平地看起来很整洁,我得再看看它。你的例子当然看起来很干净,如果它可以做我需要的。那个@instantiate 装饰器很漂亮!绝对会照顾我不喜欢的部分...
    • 我添加了一个更完整的使用属性的示例,如果您想查看的话。
    【解决方案4】:

    我想我宁愿将 dict 子类化,以便在没有可用数据的情况下恢复到默认值。像这样的:

    class fallbackdict(dict):
        ...
    
    defaults = { 'pi': 3.14 }
    x_config = fallbackdict(defaults)
    x_config.update({
        'planck': 6.62606957e-34
    })
    

    另一方面可以用可调用对象来解决。这是优雅还是丑陋取决于数据类型声明是否有用:

    pi: (float, 3.14)
    
    calc = lambda v: v[0](v[1])
    
    x_config.update({
        'planck': (double, 6.62606957e-34),
        'calculated': (lambda x: 1.0 - calc(x_config['planck']), None)
    })
    

    根据具体情况,多次使用 lambda 可能会崩溃。

    不知道是否更好,但它主要保留了字典样式。

    【讨论】:

    • 我正在考虑对 dict 进行子类化,但为了在解决其他问题时保持简单,我把它留了下来。我特别喜欢这个应用程序的 .update({}) 方法的语义。
    猜你喜欢
    • 2010-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多