2012-05-24 69 views
7

我在我的代碼中有難聞的氣味。也許我只需要讓它散發一點點,但現在它正在擾亂我。Python:結構化數據的習慣屬性?

我需要創建三個不同的輸入文件來運行三個輻射傳輸建模(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 
    ... 
} 

有很多的參數,並且它們可以清晰地分爲組,所以我想用dictdict S的:

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

我正在尋找:

一個簡單的方法來存儲其值都可以用統一的方式進行訪問,是很好的結構,並可能存在無論是作爲屬性(實際值)或配置數據屬性(從其他屬性計算)。

這是一種無聊的一種可能性:

存儲所有類型的字典的字典我前面所述,以及具有其它功能碾過物體,並計算可計算的價值?這聽起來不好玩。或乾淨。對我來說,這聽起來很混亂和令人沮喪。

難看的作品:

後很長一段時間嘗試不同的策略,且大多沒有得到哪裏,我想出了,似乎工作一個可能的解決方案:

我的班: (聞起來有點FUNC-Y,呃,質樸DEF-initely。)

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代碼並不漂亮!我一定做錯了什麼!

回答

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 

最終剪輯(?):如果你不希望有「backtrack」在子組屬性定義中使用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 

gro up_property接收基本配置對象而不是組對象,因此路徑始終從根開始。因此,e等同於之前定義的d

順便說一句,支持嵌套組是留給讀者的練習。

+0

我有種感覺,我可能不得不深入探索一些這些神奇的方法。我打算玩一下你的代碼建議,但乍看起來我很擔心'property(lambda ...'。我的一些屬性計算不適合'lambda',這就是我開始嘗試運行'def self.group.a():',雖然我現在有一些新的想法,但是謝謝! – Phil

+0

@Phil然後嘗試元類方法,它不需要你使用lambda – LaC

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 

這可能會更好。然後在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 
+0

平地似乎整潔,我將不得不考慮更多。你的例子當然看起來很乾淨,如果它能做我需要的。 '@ instantiate'裝飾器很漂亮!肯定照顧我不喜歡的東西的一部分... – Phil

+0

我添加了一個更完整的使用屬性的例子,如果你想看看。 –

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) 
}) 

視情況而定,如果是多次使用的拉姆達可能被打破了。

不知道它是否更好,但它大多保留字典樣式。

+0

我正在考慮子類化dict,但是在解決其他問題的時候,我把它放在了簡單的位置,我特別喜歡這個應用程序的'.update({})'方法的語義 – Phil

2

菲爾,

您對FUNC-Y配置猶豫是非常熟悉的我:)

我建議你到你的配置存儲而不是作爲一個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 

UPDATE

讓我們有一個使用這兩個自我,父和子組屬性的屬性config.bx在其公式中:

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 
+0

保持配置數據在一個單獨的,人類可讀的文件中似乎是一個很好的方法,我對嵌入python進入YAML的可能性感興趣,必須做一些實驗! – Phil

+0

看起來很有希望,但我不知道如何訪問變量屬於一個節點的父節點在節點中。例如,如果在你的最後一個例子中,你必須添加'a'和'c'來獲得'd' ...'d:!property self.parent.a + self.c'。我不介意總是從根引用,比如'd:!property self.a + self.b.d'。猜猜我必須閱讀更多關於'yaml.add_constructor()'。 – Phil

+2

@Phil:增加了對self.parent的支持 – spatar