views:

276

answers:

2

Being relatively new to Python 2, I'm uncertain how best to organise my class files in the most 'pythonic' way. I wouldn't be asking this but for the fact that Python seems to have quite a few ways of doing things that are very different to what I have come to expect from the languages I am used to.

Initially, I was just treating classes how I'd usually treat them in C# or PHP, which of course made me trip up all over the place when I eventually discovered the mutable values gotcha:

class Pants(object):
    pockets = 2
    pocketcontents = []

class CargoPants(Pants):
    pockets = 200

p1 = Pants()
p1.pocketcontents.append("Magical ten dollar bill")
p2 = CargoPants()

print p2.pocketcontents

Yikes! Didn't expect that!

I've spent a lot of time searching the web and through some source for other projects for hints on how best to arrange my classes, and one of the things I noticed was that people seem to declare a lot of their instance variables - mutable or otherwise - in the constructor, and also pile the default constructor arguments on quite thickly.

After developing like this for a while, I'm still left scratching my head a bit about the unfamiliarity of it. Considering the lengths to which the python language goes to to make things seem more intuitive and obvious, it seems outright odd to me in the few cases where I've got quite a lot of attributes or a lot of default constructor arguments, especially when I'm subclassing:

class ClassWithLotsOfAttributes(object):
    def __init__(self, jeebus, coolness='lots', python='isgoodfun', 
             pythonic='nebulous', duck='goose', pants=None, 
             magictenbucks=4, datawad=None, dataload=None,
             datacatastrophe=None):

        if pants is None: pants = []
        if datawad is None: datawad = []
        if dataload is None: dataload = []
        if datacatastrophe is None: datacatastrophe = []
        self.coolness = coolness
        self.python = python
        self.pythonic = pythonic
        self.duck = duck
        self.pants = pants
        self.magictenbucks = magictenbucks
        self.datawad = datawad
        self.dataload = dataload
        self.datacatastrophe = datacatastrophe
        self.bigness = None
        self.awesomeitude = None
        self.genius = None
        self.fatness = None
        self.topwise = None
        self.brillant = False
        self.strangenessfactor = 3
        self.noisiness = 12
        self.whatever = None
        self.yougettheidea = True

class Dog(ClassWithLotsOfAttributes):
    def __init__(self, coolness='lots', python='isgoodfun', pythonic='nebulous', duck='goose', pants=None, magictenbucks=4, datawad=None, dataload=None, datacatastrophe=None):
        super(ClassWithLotsOfAttributes, self).__init__(coolness, python, pythonic, duck, pants, magictenbucks, datawad, dataload, datacatastrophe)
        self.noisiness = 1000000

    def quack(self):
        print "woof"

Mild silliness aside (I can't really help myself when cooking up these artificial example classes), assuming I have a real-world need for a set of classes with this many attributes, I suppose my questions are:

  • What is the most, uhh, 'pythonic' way of declaring a class with that many attributes? Is it best to put them against the class if the default is immutable, ala Pants.pockets, or is it better to put them in the constructor, ala ClassWithLotsOfAttributes.noisiness?

  • Is there a way to eliminate the need to redeclare the defaults for all of the subclass constructor arguments, as in Dog.__init__? Should I even be including this many arguments with defaults anyway?

+5  A: 
  • If attributes will vary from instance to instance make them instance attribute i.e. create them inside__init__ using self else if they need to be shared between class instances like a constant, put them at class level.

  • If your class really need to pass, so many arguments in __init__, let derive class use argument list and keyword arguments e.g.

class Dog(ClassWithLotsOfAttributes):
    def __init__(self, *args , **kwargs):
        super(ClassWithLotsOfAttributes,    self).__init__(*args , **kwargs)
        self.coolness = "really cool!!!
  • No need of passing all variables except few important ones, in __init__, class can assume some defaults and user can change them later on if needed.
  • Use 4 spaces instead of tab.

  • if you need to add an extra arg bite, to Dog and keyword arg old too

class CoolDog(ClassWithLotsOfAttributes):
    def __init__(self, bite, *args , **kwargs):
        self.old = kwargs.pop('old', False) # this way we can access base class args too
        super(ClassWithLotsOfAttributes,    self).__init__(*args , **kwargs)
        self.bite = bite
        self.coolness = "really really cool!!!

various ways you useCoolDog

CoolDog(True)
CoolDog(True, old=False)
CoolDog(bite=True, old=True)
CoolDog(old=True, bite=False)
Anurag Uniyal
will some formatting expert, format the code correctly?
Anurag Uniyal
oh i didn't know you could use the *args and **kwargs in that way! 'coolness' most definitely needs to be upped to 'really cool!!!' :)
Shabbyrobe
also, what about the case where Dog.__init__ needs to add an extra constructor argument? is def__init__(self, legs=4, *args, **kwargs) the right way to go about it?
Shabbyrobe
yes you can specifically check of var name in kargs, i will add an example
Anurag Uniyal
“make them instance attribute i.e. create them inside__init__” -> What really matters is to use `self` instead of the class. I think it may be misleading for beginners.
Bastien Léonard
what is the main difference between self.old = kwargs.pop('old', False) and def __init__(self, bite, old=False, *args, **kwargs) ?
Shabbyrobe
no difference you can actually use that, but I added pop so that you can actually get access to base class arguments too if needed
Anurag Uniyal
@Bastien Léonard i will update the answer, thanks.
Anurag Uniyal
In the second case, `old` will take up one positional argument position (so you specify it as the second argument unless you specify arguments by name.) Extracting it from the keyword arguments dictionary, it can't be a positional argument which is sometimes desired for arguments.
Blixt
+1  A: 

It is possible that you can break your massive classes down into classes that each only perform one simple task. Usually classes don't need this many attributes.

If you really need to have that many attributes, I think you'll have to live with assigning them all too, especially since you need default values for them all. You don't need to reassign the defaults in your subclasses though (I see Anurag Uniyal showed how.)

You should assign them to self though, and not as class attributes. Note the difference:

class Class1(object):
    attr1 = 'abc'

class Class2(object):
    def __init__(self):
        self.attr1 = 'abc'

Class1.attr1 # returns 'abc'
c = Class1()
c.attr1 # Also returns 'abc'
Class1.attr1 = 'def'
c.attr1 # Returns 'def'!
c.attr1 = 'abc' # Now the c instance gets its own value and will show it
                # independently of what Class1.attr1 is. This is the same
                # behavior that Class2 exhibits from the start.
Blixt