tags:

views:

363

answers:

6

Is there a way in Python, to have more than one constructor or more than one method with the same name, who differ in the number of arguments they accept or the type(s) of one or more argument(s)?

If not, what would be the best way to handle such situations?

For an example I made up a color class. This class should only work as a basic example to discuss this, there is lot's of unnecessary and/or redundant stuff in there.

It would be nice, if I could call the constructor with different objects (a list, an other color object or three integers...) and the constructor handles them accordingly. In this basic example it works in some cases with * args and * * kwargs, but using class methods is the only general way I came up with. What would be a "best practice" like solution for this?

The constructor aside, if I would like to implement an _ _ add _ _ method too, how can I get this method to accept all of this: A plain integer (which is added to all values), three integers (where the first is added to the red value and so forth) or another color object (where both red values are added together, etc.)?

EDIT

  • I added an alternative constructor (initializer, _ _ init _ _) that basicly does all the stuff I wanted.

  • But I stick with the first one and the factory methods. Seems clearer.

  • I also added an _ _ add _ _, which does all the things mentioned above but I'm not sure if it's good style. I try to use the iteration protocol and fall back to "single value mode" instead of checking for specific types. Maybe still ugly tho.

  • I have taken a look at _ _ new _ _, thanks for the links.

  • My first quick try with it fails: I filter the rgb values from the * args and * * kwargs (is it a class, a list, etc.) then call the superclass's _ _ new _ _ with the right args (just r,g,b) to pass it along to init. The call to the 'Super(cls, self)._ _ new _ _ (....)' works, but since I generate and return the same object as the one I call from (as intended), all the original args get passed to _ _ init _ _ (working as intended), so it bails.

  • I could get rid of the _ _ init _ _ completly and set the values in the _ _ new _ _ but I don't know... feels like I'm abusing stuff here ;-) I should take a good look at metaclasses and new first I guess.

Source:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

class Color (object):

  # It's strict on what to accept, but I kinda like it that way.
  def __init__(self, r=0, g=0, b=0):
    self.r = r
    self.g = g
    self.b = b

  # Maybe this would be a better __init__?
  # The first may be more clear but this could handle way more cases...
  # I like the first more though. What do you think?
  #
  #def __init__(self, obj):
  #  self.r, self.g, self.b = list(obj)[:3]

  # This methods allows to use lists longer than 3 items (eg. rgba), where
  # 'Color(*alist)' would bail
  @classmethod
  def from_List(cls, alist):
    r, g, b = alist[:3]
    return cls(r, g, b)

  # So we could use dicts with more keys than rgb keys, where
  # 'Color(**adict)' would bail
  @classmethod
  def from_Dict(cls, adict):
    return cls(adict['r'], adict['g'], adict['b'])

  # This should theoreticaly work with every object that's iterable.
  # Maybe that's more intuitive duck typing than to rely on an object
  # to have an as_List() methode or similar.
  @classmethod
  def from_Object(cls, obj):
    return cls.from_List(list(obj))

  def __str__(self):
    return "<Color(%s, %s, %s)>" % (self.r, self.g, self.b)

  def _set_rgb(self, r, g, b):
    self.r = r
    self.g = g
    self.b = b
  def _get_rgb(self):
    return  (self.r, self.g, self.b)
  rgb = property(_get_rgb, _set_rgb)

  def as_List(self):
    return [self.r, self.g, self.b]

  def __iter__(self):
    return (c for c in (self.r, self.g, self.b))

  # We could add a single value (to all colorvalues) or a list of three
  # (or more) values (from any object supporting the iterator protocoll)
  # one for each colorvalue
  def __add__(self, obj):
    r, g, b = self.r, self.g, self.b
    try:
      ra, ga, ba = list(obj)[:3]
    except TypeError:
      ra = ga = ba = obj
    r += ra
    g += ga
    b += ba
    return Color(*Color.check_rgb(r, g, b))

  @staticmethod
  def check_rgb(*vals):
    ret = []
    for c in vals:
      c = int(c)
      c = min(c, 255)
      c = max(c, 0)
      ret.append(c)
    return ret

class ColorAlpha(Color):

  def __init__(self, r=0, g=0, b=0, alpha=255):
    Color.__init__(self, r, g, b)
    self.alpha = alpha

  def __str__(self):
    return "<Color(%s, %s, %s, %s)>" % (self.r, self.g, self.b, self.alpha)

  # ...

if __name__ == '__main__':
  l = (220, 0, 70)
  la = (57, 58, 61, 255)
  d = {'r': 220, 'g': 0, 'b':70}
  da = {'r': 57, 'g': 58, 'b':61, 'a':255}
  c = Color(); print c # <Color(0, 0, 0)>
  ca = ColorAlpha(*la); print ca # <Color(57, 58, 61, 255)>
  print '---'
  c = Color(220, 0, 70); print c # <Color(220, 0, 70)>
  c = Color(*l); print c # <Color(220, 0, 70)>
  #c = Color(*la); print c # -> Fail
  c = Color(**d); print c # <Color(220, 0, 70)>
  #c = Color(**da); print c # -> Fail
  print '---'
  c = Color.from_Object(c); print c # <Color(220, 0, 70)>
  c = Color.from_Object(ca); print c # <Color(57, 58, 61, 255)>
  c = Color.from_List(l); print c # <Color(220, 0, 70)>
  c = Color.from_List(la); print c # <Color(57, 58, 61, 255)>
  c = Color.from_Dict(d); print c # <Color(220, 0, 70)>
  c = Color.from_Dict(da); print c # <Color(57, 58, 61, 255)>
  print '---'
  print 'Check =', Color.check_rgb('1', 0x29a, -23, 40)
  # Check = [1, 255, 0, 40]
  print '%s + %s = %s' % (c, 10, c + 10)
  # <Color(57, 58, 61)> + 10 = <Color(67, 68, 71)>
  print '%s + %s = %s' % (c, ca, c + ca)
  # <Color(57, 58, 61)> + <Color(57, 58, 61, 255)> = <Color(114, 116, 122)>
A: 

You can check the type of the argument passed to your constructor inside:

def __init__(self, r = 0, g = 0, b = 0):
    # if r is a list
    if (type(r) == type([1,2,3])):
        r, g, b = r[0], r[1], r[2]
    # if r is a color
    if (type(r) == type(self)):
        r, g, b = r.r, r.g, r.b
    self.r = r
    self.g = g
    self.b = b

Maybe that will help.

kender
No, this is a bad approach -- in a dynamic language, what you care about is behavior, not types.
Brandon Corfman
Also, if you WERE to check the type of an argument it is better to use 'if isisntance(r,list)' both for clarity and performance.
Mark Roddy
What if I check if I can iterate over the first argument instead? If I can, I set the values that way. If not I don't ignore the other two and use all three like scalars. This would work for color objects too if I would implement __iter__.
Brutus
I don't know how to emphasize this enough -- don't look at types. This isn't just a dynamic language thing either ... it's like using RTTI in C++ to change class behavior. It's brittle, it doesn't scale, and it's a fundamental abuse of the language. It's the absolute opposite of "best practice".
Brandon Corfman
@Brandon: This is ideological. If you want methods that are polymorphic in all of their arguments, use whatever is available for the dispatch: length of *args, keys, and type of arguments, of course. I don't see anything brittle or non-scalable with this.
ThomasH
+7  A: 

You can have the factory methods, it is fine. But why not just call it as it is?

Color(r, g, b)
Color(*[r, g, b])
Color(**{'r': r, 'g': g, 'b': b})

This is the python way. As for the from object constructor, I would prefer something like:

Color(*Color2.as_list())

Explicit is better than implicit - Python Zen

muhuk
`Color(*Color2.as_list())` is ugly. Color has immutable semantics so there is no need to copy it and even if you'd like to copy it an explicit `c2 = c1.copy()` would be better (or `copy.copy(c1)`).
J.F. Sebastian
Well you can always have a `Color.fromColor(Color2)` class method if you find above ugly.
muhuk
+6  A: 

Python doesn't accept multiple methods with the same name, period. One method does one thing.

I've seen different approaches recommended on how to handle this ... classmethods (like you outlined above) or factory functions. I like keyword arguments the most.

class Color (object):

   def __init__(self, **parms):
      if parms.get('list'):
         self.r, self.g, self.b = parms['list'] 
      elif parms.get('color'):
         color = parms['color']
         self.r = color.r
         self.g = color.g
         self.b = color.b
      else:
         self.r = parms['red']
         self.g = parms['green']
         self.b = parms['blue']

c1 = Color(red=220, green=0, blue=270)
c2 = Color(list=[220, 0, 70])
c3 = Color(color=c1)

This fits the Python way of being explicit and readable, plus it easily allows you to add new arguments if needed.

EDIT: Plus I don't have to look at the actual constructor code to understand the arguments. The explanation is supplied by the keyword.

Brandon Corfman
A: 

Python always fully replaces methods with the same name. Unlike C# that, if I remember correctly, will make the methods with the same name options for different argument input.

If there's only a variation of one in the keywords, like either 3 or 4 arguments of the same type, I'd say using a preset of the last argument, or all of them, would be the way to go.

However, if you want lists, tuples and other types, you should probably go for the arbitrary arguments list and test the contents of that in the function

def function(*args):
    if type(args[0]) is int:
        dothis()
    #and so on
Sir Oddfellow
+6  A: 

In general, use factory methods, marked up as @classmethods. They'll also work correctly on subclasses. From a design perspective, they are more explicit, especially when given a good name.

In this case, mixing everything together is probably more convenient, but it also makes the contract for your constructor more difficult.

Torsten Marek
+2  A: 

On the __add__ issue:

First, you cannot get "three integers", I assume you mean a 3-tuple of integers?

In that case, you won't get around some isinstance calls:

def __add__(self, other):
    if isinstance(other, Color):
        ...
    elif isinstance(other, (int, long)):
        ...
    elif len(other) == 3 and all(isinstance(e, (int, long)) for e in other):
        ...
    else:
        raise TypeError("Can only add Color to Color, int or three-tuple")

You might also want to add implementations of __radd__, so that you can handle

1 + Color(1, 2, 3)

but that's just

def __radd__(self, other):
    return self.__add__(other)

although strictly, it will never be called when type(other) is Color.

Also, do not forget __iadd__ to support +=.

Torsten Marek