Reputation: 1329
Here's my use case: I want to define an object that is like a tuple, but whose elements I can access by attribute name, e.g.
mytupleobj = TupObj(2012,3)
mytupleobj.year = 2012
mytupleobj.month = 3
Pythons namedtuple
are the prime candidate for this, but the problem is that the number of arguments is fixed. So if need to only have tuple-like objects that carry the year, I either have to instaniate
mytupleobj = TupObj(2012, None)
or create a new definition of a name-tuple that only carries the year. Both solution seem ugly.
Is there a way -either using namedtuple
or some other technique- so that when I instantiate
mytupleobj = TupObj(2012)
I get a tuple-like object instantiable that has attribute year
only and when I use
mytupleobj = TupObj(2012,2)
I get a tuple-like object that has year
and month
attributes?
Upvotes: 1
Views: 128
Reputation: 9843
You can set defaults on your namedtuple
Python 2.7 Solution:
from collections import namedtuple
tupobj = namedtuple('tupobj', 'year month')
tupobj.__new__.__defaults__ = (None,) * len(tupobj._fields)
t1 = tupobj(2012)
print(t1)
# >> tupobj(year=2012, month=None)
print(t1.year)
# >> 2012
t2 = tupobj(year=2012)
print(t2)
# >> tupobj(year=2012, month=None)
print(t2.year)
# >> 2012
t3 = tupobj(month=1)
print(t3)
# >> tupobj(year=None, month=1)
print(t3.month)
# >> 1
t4 = tupobj(2012, 1)
print(t4)
# >> tupobj(year=2012, month=1)
print(t4.year)
# >> 2012
print(t4.month)
# >> 1
Python 3.7 Solution:
from collections import namedtuple
tupobj = namedtuple('tupobj', 'year month', defaults=(None,None))
t1 = tupobj(2012)
print(t1)
# >> tupobj(year=2012, month=None)
print(t1.year)
# >> 2012
t2 = tupobj(year=2012)
print(t2)
# >> tupobj(year=2012, month=None)
print(t2.year)
# >> 2012
t3 = tupobj(month=1)
print(t3)
# >> tupobj(year=None, month=1)
print(t3.month)
# >> 1
t4 = tupobj(2012, 1)
print(t4)
# >> tupobj(year=2012, month=1)
print(t4.year)
# >> 2012
print(t4.month)
# >> 1
Upvotes: 1
Reputation: 1123500
You do not want a different class definition. You merely want to make those arguments optional with a default value for the attribute if you didn't pass in a month
value. The namedtuple()
factory function doesn't support that use-case.
But that's no the only way to create a named tuple. You can also subclass typing.NamedTuple
:
from typing import NamedTuple, Optional
class VagueTimePeriod(NamedTuple):
year: int
month: Optional[int] = None
That's a class definition for a named tuple where month
is optional, if you don't specify a month it is left to the default value:
>>> VagueTimePeriod(2012)
VagueTimePeriod(year=2012, month=None)
>>> VagueTimePeriod(2012, 3)
VagueTimePeriod(year=2012, month=3)
However, I suspect that what you really want is a dataclass. A simple class that mostly just holds some data.
Python 3.7 has the new dataclasses
module, or you can install the attrs
project. A dataclass can have optional attributes (defaulting to a value you state at definition time):
from dataclasses import dataclass
from typing import Optional
@dataclass
class VagueTimePeriod:
year: int
month: Optional[int] = None
vtp1 = VagueTimePeriod(2012)
vtp2 = VagueTimePeriod(2012, 3)
Dataclasses give you greatly simplified syntax to define a small class, which comes with a representation, equality testing, and optional ordering support, hashing, and immutability.
Dataclasses also fully support inheritance, which named tuples don't.
A dataclass is not automatically iterable or immutable, but can be made so, see this previous answer of mine, where I define a simple DataclassSequence
base class that adds sequence behaviour.
A quick demo:
>>> @dataclass(frozen=True)
... class VagueTimePeriod:
... year: int
... month: Optional[int] = None
...
>>> VagueTimePeriod(2012)
VagueTimePeriod(year=2012, month=None)
VagueTimePeriod(2012, 3)
VagueTimePeriod(year=2012, month=3)
Upvotes: 1
Reputation: 6246
If your goal is to have an easy to understand implementation of how this would look from an OOP perspective, you just need to work with conditionals and pass default arguments for values, checking for their conditional value during the init. It may look something like this:
class mydatetimeclass():
def __init__(self, year, month=None, day=None):
self.year = year
if month is not None:
self.month = month
if day is not None:
self.day = day
obj1 = mydatetimeclass(2016)
obj1.year #2016
obj2 = mydatetimeclass(2017, 5)
obj2.year #2017
obj2.month #5
Another cleaner to implement/maintain approach can be to just have the default values saved as None, so that you do not have to worry about which attributes actually exist in each object.
class mydatetimeclass():
def __init__(self, year, month=None, day=None):
self.year = year
self.month = month #sets to None by default
self.day = day
Upvotes: 1