16

I'm pretty new to Python, so if there's anything here that's flat-out bad, please point it out.

I have an object with this dictionary:

traits = {'happy': 0, 'worker': 0, 'honest': 0}

The value for each trait should be an int in the range 1-10, and new traits should not be allowed to be added. I want getter/setters so I can make sure these constraints are being kept. Here's how I made the getter and setter now:

def getTrait(self, key):
    if key not in self.traits.keys():
        raise KeyError

    return traits[key]

def setTrait(self, key, value):
    if key not in self.traits.keys():
        raise KeyError

    value = int(value)

    if value < 1 or value > 10:
        raise ValueError

    traits[key] = value

I read on this website about the property() method. But I don't see an easy way to make use of it for getting/setting the values inside the dictionary. Is there a better way to do this? Ideally I would like the usage of this object to be obj.traits['happy'] = 14, which would invoke my setter method and throw a ValueError since 14 is over 10.

illright
  • 3,991
  • 2
  • 29
  • 54
jb.
  • 9,921
  • 12
  • 54
  • 90
  • Would you be OK with `obj.traits` as an object, so you could get/set `obj.traits.happy`? – Daniel Roseman Oct 13 '11 at 22:00
  • @DanielRoseman, You mean make traits its own separate object with a 'happy' member? Hadn't thought of it, but it would work, seems like overkill though. – jb. Oct 13 '11 at 22:04
  • Have you looked into http://code.enthought.com/projects/traits/? Also, why do you *need* to store the values in a dictionary instead of them being properties on your object? – millimoose Oct 13 '11 at 22:14
  • @Sii, I don't need the dictionary, we're going to have lots more traits, so I was just thinking of an easy way to keep them "together" but each with their own value. If you want to whip up a code example that is better than this, I'll seriously consider it. Like I said, I'm new to Python :) – jb. Oct 13 '11 at 23:09
  • You might want to be careful about `value = int(value)`. That will throw some kind of exception (can't remember what) if `value` can't be turned into an integer, e.g. `int('dog')`. However, note also that `int(2.5)` returns `2`, so if that's not what you want check that `int(value) == float(value)` as well. – andronikus Oct 13 '11 at 23:13
  • @jb.: a object is for keeping vaulues together… –  Oct 13 '11 at 23:41
  • @jb if you don't need the traits dictionary, then you should be able to use `property()` for this. Overriding what happens when an object attribute is read / set is *exactly* what they're for. As hop said, keep the values in your object, and store trait metadata separately. – millimoose Oct 14 '11 at 01:11

3 Answers3

13

If you are willing to use syntax like obj['happy'] = 14 then you could use __getitem__ and __setitem__:

def __getitem__(self, key):
    if key not in self.traits.keys():
        raise KeyError
    ... 
    return traits[key]

def __setitem__(self, key, value):
    if key not in self.traits.keys():
        raise KeyError
    ...
    traits[key] = value

If you really do want obj.traits['happy'] = 14 then you could define a subclass of dict and make obj.traits an instance of this subclass. The subclass would then override __getitem__ and __setitem__ (see below).

PS. To subclass dict, inherit from both collections.MutableMapping, and dict. Otherwise, dict.update would not call the new __setitem__.

import collections
class TraitsDict(collections.MutableMapping,dict):
    def __getitem__(self,key):
        return dict.__getitem__(self,key)
    def __setitem__(self, key, value):
        value = int(value)
        if not 1 <= value <= 10:
            raise ValueError('{v} not in range [1,10]'.format(v=value))
        dict.__setitem__(self,key,value)
    def __delitem__(self, key):
        dict.__delitem__(self,key)
    def __iter__(self):
        return dict.__iter__(self)
    def __len__(self):
        return dict.__len__(self)
    def __contains__(self, x):
        return dict.__contains__(self,x)

class Person(object):
    def __init__(self):
        self.traits=TraitsDict({'happy': 0, 'worker': 0, 'honest': 0})

p=Person()
print(p.traits['happy'])
# 0

p.traits['happy']=1
print(p.traits['happy'])
# 1

p.traits['happy']=14
# ValueError: 14 not in range [1,10]
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Nice. Would I have to 'bind' it to the traits dictionary somehow? – jb. Oct 13 '11 at 22:03
  • 2
    Also you can write `if not 1 < value < 10` instead of `or` condition. – refaim Oct 13 '11 at 22:05
  • Thanks for the improvement, @refaim. – unutbu Oct 13 '11 at 22:13
  • Why use `self.keys()` instead of just `self`? Also this code isn't working for me (`KeyError` on line 5 for any existent or non-existent keys). – refaim Oct 13 '11 at 22:17
  • @unutbu Ok, this is working so far, but when I do `for key in self.traits.keys()`, `keys()` is returning an empty list. Do I need to override the keys method as well? – jb. Oct 14 '11 at 00:26
  • @jb. I added `__iter__`, which makes `key()` work. I also added other methods which `MutableMapping` requires be overridden. I think that should suffice, but please tell me if you find `TraitsDict` not acting like a `dict`. – unutbu Oct 14 '11 at 00:55
  • @unutbu, but I have 2 different dictionaries under same class, not single one – Adir Dayan Nov 21 '22 at 11:27
3

Some obvious tips come to my mind first:

  1. Do not use .keys() method when checking for existence of some key (instead of if key not in self.traits.keys() use if key not in self.traits).
  2. Do not explicitly throw KeyError exception - it is thrown if you try to access inexistent key.

Your code could look like this after above changes:

def getTrait(self, key):
    return traits[key]

def setTrait(self, key, value):
    if key not in self.traits:
        raise KeyError

    value = int(value)

    if value < 1 or value > 10:
        raise ValueError

    traits[key] = value

Ps. I did no check the correctness of your code thoroughly - there may be some other issues.

Tadeck
  • 132,510
  • 28
  • 152
  • 198
1

and new traits should not be allowed to be added.

The natural way to do this is to use an object instead of a dictionary, and set the class' __slots__.

The value for each trait should be an int in the range 1-10... I want getter/setters so I can make sure these constraints are being kept.

The natural way to do this is to use an object instead of a dictionary, so that you can write getter/setter logic that's part of the class, and wrap them up as properties. Since all these properties will work the same way, we can do some refactoring to write code that generates a property given an attribute name.

The following is probably over-engineered:

def one_to_ten(attr):
  def get(obj): return getattr(obj, attr)
  def set(obj, val):
    val = int(val)
    if not 1 <= val <= 10: raise ValueError
    setattr(obj, attr, val)
  return property(get, set)

def create_traits_class(*traits):
  class Traits(object):
    __slots__ = ['_' + trait for trait in traits]
    for trait in traits: locals()[trait] = one_to_ten('_' + trait)
    def __init__(self, **kwargs):
      for k, v in kwargs.items(): setattr(self, k, v)
      for trait in traits: assert hasattr(self, trait), "Missing trait in init"
    def __repr__(self):
      return 'Traits(%s)' % ', '.join(
        '%s = %s' % (trait, getattr(self, trait)) for trait in traits
      )
  return Traits

example_type = create_traits_class('happy', 'worker', 'honest')
example_instance = example_type(happy=3, worker=8, honest=4)
# and you can set the .traits of some other object to example_instance.
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153