7

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

我需要创建三个不同的输入文件来运行三个辐射传递模型 (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
    ...
}

参数很多,而且可以很干净的归类,所以我想到了在 s 中使用dicts dict

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

我喜欢。但是有很多多余的属性。例如,如果知道对方位角和天顶角,就可以找到太阳方位角和天顶角,那么为什么要对两者都进行硬编码呢?所以我开始研究 python 的 builtin 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代码不漂亮!我一定做错了什么!

4

4 回答 4

2

菲尔,

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

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

配置文件(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.bx,它在其公式中同时使用 self、parent 和 subgroup 属性:

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
于 2012-05-25T04:46:13.170 回答
1

哇,我今天刚读到一篇关于 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

那可能会更好。然后创建一个不错__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
于 2012-05-25T00:02:59.267 回答
1

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

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.

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

于 2012-05-25T00:19:07.697 回答
0

我想我宁愿将 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,它可能会被破坏。

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

于 2012-05-25T00:52:22.527 回答