2012-05-24 10 views
7

Ho un cattivo odore nel mio codice. Forse ho solo bisogno di lasciarlo andare in aria per un po ', ma in questo momento mi sta tormentando.Python: proprietà idiomatiche per i dati strutturati?

Ho bisogno di creare tre diversi file di input per eseguire tre applicazioni di RTR (Radiative Transfer Modeling), in modo da poter confrontare i loro output. Questo processo verrà ripetuto per migliaia di insiemi di input, quindi lo sto automatizzando con uno script python.

Vorrei memorizzare i parametri di input come un oggetto python generico che posso passare a tre altre funzioni, ognuna delle quali tradurrà quell'oggetto generale nei parametri specifici necessari per eseguire il software RTM di cui sono responsabili. Penso che abbia senso, ma sentiti libero di criticare il mio approccio.

Ci sono molti parametri di input possibili per ogni parte del software RTM. Molti di loro si sovrappongono. La maggior parte di essi è mantenuta a valori predefiniti, ma dovrebbe essere facilmente modificata.

Ho iniziato con un semplice dict

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

Ci sono un sacco di parametri, e possono essere in modo pulito classificati in gruppi, così ho pensato di usare dict s all'interno della dict:

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

Mi piace. Ma ci sono molte proprietà ridondanti. L'azimut solare e gli angoli zenitali, ad esempio, possono essere trovati se l'altro è noto, quindi perché hard-code entrambi? Così ho iniziato a cercare all'interno di python property. Che mi permette di fare cose eccezionali con i dati, se posso conservare come gli attributi degli oggetti:

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

Ma ora ho perso la struttura che ho avuto dalla seconda dict esempio.

Si noti che alcune delle proprietà sono meno banali del mio esempio solar_zenith_angle e potrebbero richiedere l'accesso ad altri attributi al di fuori del gruppo di attributi di cui fa parte. Ad esempio, posso calcolare solar_azimuth_angle se conosco il giorno dell'anno, l'ora del giorno, la latitudine e la longitudine.

Quello che sto cercando:

Un modo semplice per memorizzare i dati di configurazione di cui tutti i valori si può accedere in modo uniforme, sono ben strutturati, e possono esistere sia come attributi (valori reali) o proprietà (calcolate da altri attributi).

Una possibilità che è una specie di noioso:

Conservare tutto nel dict di dicts ho delineato in precedenza, e con altre funzioni corrono sopra l'oggetto e calcolare i valori calcolabile? Questo non sembra divertente. O pulito. A me sembra disordinato e frustrante.

Un brutto quello che funziona:

Dopo molto tempo cercando strategie diverse e per lo più ottenere nessun dove, mi si avvicinò con una possibile soluzione che sembra funzionare:

Le mie lezioni: (odori un po 'func-y, ehm, funky 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?! 

Come li posso usare: (opere come vorrei)

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) 

Il comportamento: (uscita di esecuzione di tutto il codice di cui sopra, come ci si aspettava)

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) 

Perché non piace:

La memorizzazione dei dati di configurazione nelle definizioni di classe all'interno dell'oggetto __init__() dell'oggetto principale non è elegante. Soprattutto dover istanziarli immediatamente dopo una definizione del genere. Ugh. Posso occuparmene per la classe genitore, certo, ma farlo in un costruttore ...

Memorizzare le stesse classi al di fuori dell'oggetto principale Configuration non si sente elegante neanche, poiché le proprietà nelle classi interne possono dipendere da gli attributi di Configuration (oi loro fratelli al suo interno).

ho potuto fare con la definizione delle funzioni al di fuori di tutto, in modo da avere dentro cose come

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

ma io non riesco a capire come fare qualcosa di simile

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

(quando provo per essere intelligente su di esso mi imbatto sempre in <property object at 0xXXXXX>)

Quindi qual è la strada giusta da fare per questo? Mi manca qualcosa di base o un approccio molto sbagliato? Qualcuno conosce una soluzione intelligente?

Help! Il mio codice Python non è bello! Devo fare qualcosa di sbagliato!

risposta

1

Bene, ecco un modo brutto per almeno assicurarsi che le proprietà vengono chiamati:

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) 

Naturalmente, a questo punto si potrebbe anche rinunciare property del tutto e basta controllare se l'attributo è chiamabile in __getattribute__.

Oppure si potrebbe andare tutti fuori e divertirsi con metaclassi:

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 

usare in questo modo:

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

montaggio finale (?): Se non si vuole avere a "marcia indietro" utilizzando self.config nelle definizioni di proprietà sottogruppo, è possibile utilizzare il seguente comando:

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 riceve l'oggetto di configurazione di base anziché l'oggetto di gruppo, quindi i percorsi iniziano sempre dalla radice. Pertanto, e equivale allo d definito in precedenza.

BTW, il supporto di gruppi nidificati viene lasciato come esercizio per il lettore.

+0

Avevo la sensazione che probabilmente avrei dovuto approfondire alcuni di quei metodi magici. Ho intenzione di giocare un po 'con i suggerimenti del codice, ma a prima vista sono preoccupato dalla 'proprietà (lambda ...'. Alcuni dei miei calcoli di proprietà non rientrano in un 'lambda', ed è qui che Comincio a correre in circolo cercando di fare 'def self.group.a():' Anche se ho qualche nuova idea con cui giocare ora, grazie! – Phil

+0

@Phil prova l'approccio metaclass, quindi. Non richiede di usare lambda. – LaC

1

Wow, ho appena letto un articolo sui descrittori su r/python oggi, ma non penso che i descrittori di hacking possano darti quello che vuoi.

L'unica cosa che so che gestisce le sotto-configurazioni come quella è flatland. Ecco come funzionerebbe comunque in Flatlandia.

Ma si potrebbe fare:

class Configuration(Form): 
    day_of_year = Integer 
    time_of_day = Integer 

    class solar(Form): 
     azimuth_angle = Integer 
     solar_angle = Integer 

quindi caricare il dizionario in

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

amo pianura, ma non sono sicuro si guadagna molto usandolo.

È possibile aggiungere un metaclasse o un decoratore alla definizione della classe.

qualcosa come

def instantiate(klass): 
    return klass() 

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

che potrebbe essere migliore. Quindi crea un bel __init__ su Configurazione che può caricare tutti i dati da un dizionario. Non so se qualcun altro ha un'idea migliore.

Ecco qualcosa di un po 'più completo (senza la stessa magia della risposta di LaC, ma leggermente meno generico).

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

Flatlandia sembra pulito, dovrò guardare in più. Il tuo esempio ci è sembrato certamente pulito, se può fare ciò di cui ho bisogno. Quel decoratore '@ instantiate' è elegante! Sicuramente mi occupo di una parte di ciò che non mi piaceva di quello che avevo ... – Phil

+0

Ho aggiunto un esempio più completo che usa le proprietà, se vuoi guardarlo. –

0

Penso che preferirei sottoclasse dict in modo che ricada su un valore predefinito se nessun dato era disponibile. Qualcosa del genere:

class fallbackdict(dict): 
    ... 

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

L'altro aspetto può essere affrontato con callables. Che sia questo è elegante o brutto dipende da dichiarazioni tipo di dati wether sono utili:

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

A seconda delle circostanze, il lambda potrebbe essere scoppiata se è usato molte volte.

Non so se è meglio, ma conserva principalmente lo stile del dizionario.

+0

Stavo considerando la sottoclasse di dict, ma l'ho lasciato fuori per mantenere le cose semplici mentre lavoravo agli altri nodi.Mi piace particolarmente la semantica del metodo '.update ({})' per questa applicazione. – Phil

2

Phil,

tua esitazione a func-y config è molto familiare per me :)

suggerisco di memorizzare la vostra configurazione non come un file python, ma come un file di dati strutturati. Personalmente preferisco YAML perché sembra pulito, proprio come hai progettato all'inizio. Ovviamente, dovrai fornire formule per le proprietà calcolate automaticamente, ma non è male se non inserisci troppo codice. Ecco la mia implementazione usando PyYAML lib.

Il file di configurazione (config.yml):

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

Il codice:

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

L'output:

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) 

La configurazione può essere di qualsiasi profondità e proprietà, utilizzare qualsiasi valore di sottogruppo. Prova questo per esempio:

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 

Supponendo che già caricato questa configurazione:

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

Diamo uno config.bx proprietà che utilizza sia auto, genitore e gli attributi sottogruppi nella sua formula:

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

Poi abbiamo solo bisogno di aggiungere un riferimento a genitori in sottogruppi:

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

E vediamo come funziona:

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

Mantenere la configurazione i dati in un file separato, leggibile dall'uomo, sembrano un ottimo approccio Sono incuriosito dalla possibilità di incorporare Python in YAML, che dovrà fare alcuni esperimenti! – Phil

+0

Sembra promettente, ma non riesco a capire come accedere variabili appartenenti al genitore di un nodo da con nel nodo. Quindi, per esempio, se nel tuo ultimo esempio dovevi aggiungere 'a' e' c' per ottenere 'd' ...' d:! Proprietà self.parent.a + self.c '. Non mi dispiacerebbe fare sempre riferimento alla radice, qualcosa come 'd:! Proprietà self.a + self.b.d'. Suppongo che dovrò leggere di più su 'yaml.add_constructor()'. – Phil

+2

@Phil: aggiunto supporto per self.parent – spatar

Problemi correlati