### These are some means of creating multiple construction options
### for objects in Python:
### - Variable arguments
### - Keyword arguments
### - Factory methods
### We will illustrate each of these with a BankAccount class:

### Variable arguments:
##class BankAccount:
##    '''BankAccount objects have a string name and an integer balance.
##If the name isn't supplied, string '(no name)' is used.
##If the balance isn't supplied, a 0 balance is assumed.'''
##    name = '(no name)'
##    balance = 0
##    
##    def __init__(self, *argv):
##        for arg in argv:
##            if isinstance(arg, str):
##                self.name = arg
##            elif isinstance(arg, int):
##                self.balance = arg
##
##    def __str__(self):
##        return "BankAccount[name='{0}',balance={1}]".format(self.name, self.balance)
##    # etc.        
##            
##print(BankAccount())
##print(BankAccount('Todd Neller'))
##print(BankAccount(42))
##print(BankAccount('Todd Neller', 42))
##print(BankAccount(42, 'Todd Neller'))
### pro: flexible; con: requires arguments to be of different types

# Keyword arguments:
class BankAccount:
    '''BankAccount objects have a string name and an integer balance.
If the name isn't supplied, string '(no name)' is used.
If the balance isn't supplied, a 0 balance is assumed.'''
    
    def __init__(self, name='(no name)', balance=0):
        self.name = name
        self.balance = balance

    def __str__(self):
        return "BankAccount[name='{0}',balance={1}]".format(self.name, self.balance)
    # etc.        
            
print(BankAccount())
print(BankAccount(name='Todd Neller'))
print(BankAccount(balance=42))
print(BankAccount('Todd Neller', 42))
print(BankAccount(42, 'Todd Neller'), 'Oops!') #problem - args assumed in order if keywords not used
print(BankAccount(balance=42, name='Todd Neller'))
# pro: clear, clean; con: must either use keywords, or be sensitive to ordering
# and default values

### Factory methods:
##class BankAccount:
##    '''BankAccount objects have a string name and an integer balance.
##If the name isn't supplied, string '(no name)' is used.
##If the balance isn't supplied, a 0 balance is assumed.'''
##    
##    def __init__(self):
##        '''Create a BankAccount with default values.'''
##        self.name = '(no name)'
##        self.balance = 0
##
##    @classmethod
##    def createWithName(cls, name): # factory method
##        '''Create a BankAccount with a given name and 0 balance.'''
##        ba = cls()
##        ba.name = name
##        return ba
##
##    @classmethod
##    def createWithNameAndBalance(cls, name, balance): # factory method
##        '''Create a BankAccount with a given name and balance.'''
##        ba = cls()
##        ba.name = name
##        ba.balance = balance
##        return ba
##
##    @classmethod
##    def createWithBalance(cls, balance): # factory method
##        '''Create a BankAccount with a given balance and name '(no name)'.'''
##        ba = cls()
##        ba.balance = balance
##        return ba
##
##    def __str__(self):
##        return "BankAccount[name='{0}',balance={1}]".format(self.name, self.balance)
##    # etc.        
##            
##print(BankAccount())
##print(BankAccount.createWithName('Todd Neller'))
##print(BankAccount.createWithBalance(42))
##print(BankAccount.createWithNameAndBalance('Todd Neller', 42))
### pro: explicit; con: verbose, cumbersome

# my preference: keywords with default values

