Parameter lists

Bruno Desthuilliers bdesth.quelquechose at free.quelquepart.fr
Mon Feb 5 18:01:12 EST 2007


Steven D'Aprano a écrit :
 > On Sun, 04 Feb 2007 17:45:04 +0100, Mizipzor wrote:
 >
 >
 >>Consider the following snippet of code:
 >>
 >>class Stats:
 >>    def __init__(self, speed, maxHp, armor, strength, attackSpeed, 
imagePath):
 >>        self.speed = speed
 >>        self.maxHp = maxHp
 >>        self.armor = armor
 >>        self.strength = strength
 >>        self.attackSpeed = attackSpeed
 >>        self.originalImage = loadTexture(imagePath)
 >>
 >>
 >>I little container for holding the stats for some rpg character or
 >>something. Now, I dont like the looks of that code, there are many
 >>function parameters to be sent in and if I were to add an attribute, i
 >>would need to add it in three places. Add it to the function
 >>parameters, add it to the class and assign it.
 >>
 >>Is there a smoother way to do this? There usually is in python, hehe.
 >
 >
(snip)
 >
 > def __init__(self, **kwargs):
 >     for key in kwargs:
 >         if hasattr(self, key):
 >             # key clashes with an existing method or attribute
 >             raise ValueError("Attribute clash for '%s'" % key)
 >     self.__dict__.update(kwargs)
 >
 >
 > === Advantages ===
 >
(snip)
 >
 > === Disadvantages ===
 >
(snip)
 > (2) Typos can cause strange bugs which are hard to find:
 >
 > Stats(armour="chainmail", stealth=2, stregnth=4, ...)
 >
 > Now your character is unexpectedly strong because it inherits the 
default,
 > and you don't know why.


 > (3) Easy to break your class functionality:
 >
 > Stats(name_that_clashes_with_a_method="something else", ...)
 >

How to overcome these two problem - just overcomplexifying things a bit:

class StatsAttribute(object):
   def __init__(self, default=None):
     self._default = default
     self._attrname = None # set by the StatType metaclass

   def __get__(self, instance, cls):
     if instance is None:
         return self
     return instance._stats.get(self._attrname, self._default)

   def __set__(self, instance, value):
     instance._stats[self._attrname] = value

class StatsType(type):
   def __init__(cls, name, bases, attribs):
     super(StatsType, cls).__init__(name, bases, attribs)
     statskeys = getattr(cls, '_statskeys', set())
     for name, attrib in attribs.items():
       if isinstance(attrib, StatsAttribute):
         # sets the name to be used to get/set
         # values in the instance's _stats dict.
         attrib._attrname = name
         # and store it so we know this is
         # an expected attribute name
         statskeys.add(name)
     cls._statskeys = statskeys

class Stats(object):
   __metaclass__ = StatsType

   def __init__(self, **stats):
     self._stats = dict()
     for name, value in stats.items():
       if name not in self._statskeys:
         # fixes disadvantage #2 : we won't have unexpected kw args
         msg = "%s() got an unexpected keyword argument '%s'" \
               % (self.__class__.__name__, name)
         raise TypeError(msg)
       setattr(self, name, value)

# just a dummy object, I didn't like the
# idea of using strings litterals for things
# like armors or weapons... And after all,
# it's supposed to be overcomplexified, isn't it ?
class _dummy(object):
   def __init__(self, **kw):
     self._kw = kw

   def __getattr__(self, name):
     return self._kw[name]

class Leather(_dummy): pass
class Sword(_dummy): pass
class FullPlate(_dummy): pass
class MagicTwoHanded(_dummy): pass

# let's go:
class Warrior(Stats):
   # fixes disatvantage 3 : we won't have name clash
   strength = StatsAttribute(default=12)
   armour = StatsAttribute(default=Leather())
   weapon = StatsAttribute(default=Sword())

bigBill = Warrior(
   strength=120,
   armour=FullPlate(),
   weapon=MagicTwoHanded(bonus=20)
   )

try:
   wontDo = Warrior(
     sex_appeal = None
     )
except Exception, e:
   print "got : %s" % e


Did I won a MasterProgrammer (or at least a SeasonnedPro) award ?-)
http://sunsite.nus.sg/pub/humour/prog-evolve.html

Err... me go to bed now...

 > If you've got lots of attributes, you're better off moving them to
 > something like a INI file and reading from that:
 >
 > class Stats:
 >     defaults = "C:/path/defaults.ini"
 >     def __init__(self, filename=None, **kwargs):
 >         if not filename:
 >             filename = self.__class__.defaults
 >         self.get_defaults(filename) # an exercise for the reader
 >         for key in kwargs:
 >             if not self.__dict__.has_key(key):
 >                 raise ValueError("Unknown attribute '%s' given" % key)
 >         self.__dict__.update(kwargs)

And then allow for Python source code in the INI file (that will be used
to create methods) to specify behaviour ?-)

Ok, this time I really go to bed !-)




More information about the Python-list mailing list