简体   繁体   中英

Using a different class definition depending on the constructor signature

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, eg

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?

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

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)

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM