python自定义一个日期类

        通过前面我们对python中魔法方法的了解,现在我们综合运用这些魔法方法来自定义一个日期类,加深我们对魔法方法的理解和运用。

日期类

        首先我们来写一个__init__方法,用来初始化日期类。在__init__中我们可以使用缺省参数,来防止出现实例化时忘记传参不能实例化的情况。日期类中的年、月、日属性可以使用双下划线来隐藏,防止别人随意修改属性值。

class MyDate:
    """日期类"""

    def __init__(self, year=2023, month=1, day=1):
        self.__year = year
        self.__month = month
        self.__day = day

 现在我们就可以通过实例化MyDate来得到日期对象了,随便传入一些参数来得到日期对象。

date1 = MyDate()
print(date1)
date2 = MyDate(1999, 6, 6)
print(date2)

 执行结果如下:

我们发现用print打印出来的对象信息不好看,如果能打印出<年-月-日>就好了。因此我们就想到了__str__方法,print是调用__str__方法来打印对象信息的。我们自定义__str__方法就可以解决这个问题了,于是我们在MyDate类中写出__str__方法。 在__str__方法中,我们需要返回包含年、月、日信息的字符串。月和日我们可以做一下处理,让它们保持两位数,不足两位时在前面添0补齐(例如:07月06日)。

    def __str__(self):
        return f'{self.__year}-{self.__month:0>2}-{self.__day:0>2}'

我们添加完__str__方法方法后,再次运行程序,打印出对象信息。

现在看起来是不是舒服多了。

判断日期

        虽然我们已经实现了日期对象的创建,但是这里有一个BUG,我们可以创建任意的时间,例如2023年1月60日。

date1 = MyDate(2023, 1, 60)
print(date1)

 执行结果如下:

像2023年1月60日这种日期根本就不存在,也不符合日期的规则。因此我们需要对日期做判断,判断这个日期是否符合公历标准。符合公历标准我们才创建日期对象,不符合公历标准就抛出错误。

        我们的公历日期年份要大于等于0,月份为1~12,每月的天数根据年份和月份共同决定。我们可以发现年份和月份都很好判断,唯独每月的天数不好判断,但别害怕我们慢慢分析。年分为平年和闰年的,平年2月28天,闰年2月29天。其他月份都一样,1月31天、3月31天、4月30天、5月31天、6月30天、7月31天、8月31天、9月30天、10月31天、11月30天、12月30天。根据月份天数信息我们可以设计出一个月份列表,把每一个月的天数都放到列表索引对应的位置,我们可以得到一个平年月份列表和一个闰年月份列表。

    __non_leap = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # 平年月份列表
    __leap = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # 闰年月份列表

因为列表索引是从0开始的,我们又没有0月份,所以第一个元素用0占位,我们也用不到第一个元素。月份列表同样使用双下划线隐藏,防止被随意修改。

        然后我们就要来区分平年和闰年,平年使用平年列表,闰年使用闰年列表。那平年和闰年是如何区分的呢?年份能被4整除但不能被100整除,或者能被400整除的称为闰年,除了闰年其余的都是平年。因此我们可以只判断闰年,如果是闰年就使用闰年月份列表,否则就使用平年月份列表。于是我们在MyDate类中写了一个用于返回月份天数的方法,如下:

    __non_leap = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # 平年月份列表
    __leap = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # 闰年月份列表

    def _month_day(self):
        """
        返回这个月的天数\n
        :return: 月份的天数
        """
        if (self.__year % 4 == 0 and self.__year % 100 != 0) or (self.__year % 400 == 0):
            return self.__leap[self.__month]  # 返回闰年月份的天数
        return self.__non_leap[self.__month]  # 返回平年月份的天数

写完这个方法后,我们发现它其实有点冗余。平年月份列表和闰年月份列表之间,就一个2月相差了一天,我们完全可以只用一个列表就够了。我们在if判断条件中再加一个条件,当前月份是否为2月。如果月份是2月并且年份为闰年,就返回29天,否则就返回平年月份列表中对应月份的天数。于是我们对_month_day()方法进行修改,改成下面的样子:

    __non_leap = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # 平年月份列表

    def _month_day(self):
        """
        返回这个月的天数\n
        :return: 月份的天数
        """
        if self.__month == 2 and ((self.__year % 4 == 0 and self.__year % 100 != 0) or (self.__year % 400 == 0)):
            return 29
        return self.__non_leap[self.__month]  # 返回平年月份的天数

有了这个判断月份天数的函数,我们就可以在初始化时判断这个日期是否符合公历标准。于是我们在__init__方法中增加判断日期的逻辑语句:

    def __init__(self, year=2023, month=1, day=1):
        self.__year = year
        self.__month = month
        self.__day = day
        if not (self.__year >= 0 and (0 < self.__month < 13) and (0 < self.__day <= self._month_day())):
            raise ValueError

现在我们就可以用MyDate类实例化出正确的日期了,如果日期不正确,实例化会失败。

date1 = MyDate()
print(date1)
date2 = MyDate(2023, 1, 60)
print(date2)

执行结果如下:

虽然现在可以抛出错误了,但是抛出的错误ValueError跟我们的MyDate类型不是很搭。而且只是抛出ValueError没有文字描述,也让人很疑惑不知为什么报错。所以我们可以自定义错误类型,抛出符合MyDate类型的错误。

自定义日期错误类型

        python中的错误类型基本上都是继承的Exception,我们自定义错误也是通过继承Exception。我们可以把错误日期传递到错误类型的实例中,这时就需要自定义__init__方法来接收错误日期,并把错误日期赋值给实例属性;然后再自定义__str__方法来描述错误内容,并把错误日期添加到描述内容中。于是我们可以写出如下错误类型:

class MyDateError(Exception):
    """非法日期错误"""

    def __init__(self, error_date):
        self.date = error_date

    def __str__(self):
        return f"The data should conform to the gregorian standards\nno exists date {self.date}"

        现在我们把__init__方法中抛出ValueError改为抛出MyDateError。

    def __init__(self, year=2023, month=1, day=1):
        self.__year = int(year)
        self.__month = int(month)
        self.__day = int(day)
        if not (self.__year >= 0 and (0 < self.__month < 13) and (0 < self.__day <= self._month_day())):
            raise MyDateError(f'{self.__year}-{self.__month:0>2}-{self.__day:0>2}')

我们再来实例化错误日期,查看报错。

这种报错方式是不是就好多了。

日期大小比较

        MyDate的雏形已经出来了,现在我们可以赋予它同类之间比较大小的能力。于是我们想到了用于比较运算的魔法方法:__eq__、__ne__、__lt__、__le__、__gt__、__ge__。我们只要在MyDate中实现了这几个魔法方法,就可以让MyDate的实例进行比较运算了。

__eq__(等于)

        首先我们需要思考两个日期是否相等,需要比较那些内容。应该需要年、月、日这3个属性是否相等,如果年、月、日都相等那我们可以说这两个日期相等。于是我们在类中定义__eq__方法。

    def __eq__(self, other):
        return self.__year == other.__year and self.__month == other.__month and self.__day == other.__day

我们来试一下MyDate的实例能否做等于运算。

date1 = MyDate()
date2 = MyDate(2023, 1, 30)
date3 = MyDate(2023, 1, 30)
print(date2 == date1)
print(date2 == date3)

执行结果如下:

我们可以看到能正确的判断出两个日期是否相等,但是如果我们用其他的类型来和MyDate比较会怎么样呢?

date1 = MyDate()
print(date1 == 1)

执行结果如下:

我们可以看到报错了,不但报错了而且还暴露了我们的隐藏属性_MyDate__year。所以我们为了避免这种报错,我们需要在比较之前判断一下参数other的类型是不是MyDate,如果是我们才比较,如果不是就直接抛出错误。于是我们就应该在__eq__方法中增加判断other是否为MyDate类型的逻辑,但是先别慌。好好想一下,既然等于需要判断类型,那不等于是不是要判断类型,小于、大于、小于等于、大于等于是不是也要判断类型。既然所有的比较方法中都需要判断类型,那我们每一个比较方法中都要写一段判断类型的代码。这样做就会让我们的代码变得冗余,那有没有减少代码冗余的方式。于是我们就想到了装饰器,完全可以使用装饰器来搞定类型的判断,减少代码重复度。我们就写出了如下装饰函数:

from functools import wraps


class NoMyDateError(Exception):
    """不是MyDate类型错误"""

    def __str__(self):
        return "MyDate can only be compared to MyDate"


def check_type(func):
    """
    检查比较的类型是否为MyDate的装饰器\n
    如果不是抛出 NoMyDateError\n
    :param func: 被装饰的函数
    :return:
    """
    @wraps(func)
    def check(*args, **kwargs):
        for _type in args:
            if type(_type) != MyDate:
                raise NoMyDateError
        result = func(*args, **kwargs)
        return result
    return check

在装饰函数中抛出错误的时候,又顺带定义了一个错误。装饰函数写好了,我们就把它装饰到比较方法上吧。

    @check_type
    def __eq__(self, other):
        return self.__year == other.__year and self.__month == other.__month and self.__day == other.__day

现在我们再来和其他类型比较,查看报错如何显示。

现在这种报错是不是好多了,也不会再暴露我们的隐藏属性了。

__ne__(不等于)

        既然我们实现了等于,那不等于就简单了。因为等于的反面就是不等于,使用逻辑运算符not就轻松搞定了。

    @check_type
    def __ne__(self, other):
        return not self == other

我们在__ne__中进行==运算,在把运算结果取反,就得到了!=的运算结果了。

__lt__(小于)

        首先我们思考一下,怎样判断两个日期的大小。应该是年比较小的日期小;当年相等时再比较月,月比较小的日期小;当年和月都相等时再比较天数,天数少的日期小。于是我们在MyDate中定义出__lt__方法:

    @check_type
    def __lt__(self, other):
        if self.__year < other.__year:
            return True
        elif self.__year == other.__year and self.__month < other.__month:
            return True
        elif self.__year == other.__year and self.__month == other.__month and self.__day < other.__day:
            return True
        return False

__le__(小于等于)

        我们已经实现了等于和小于方法,那小于等于就简单了。因为小于等于就是小于或等于,使用逻辑运算符or把小于和等于的结果处理一下就行了。

    @check_type
    def __le__(self, other):
        return self < other or self == other

__gt__(大于)

        实现了小于等于,大于就简单了,就是小于等于的反面,使用逻辑运算符not就搞定了。

    @check_type
    def __gt__(self, other):
        return not self <= other

__ge__(大于等于)

        大于等于就是小于的反面,我们已经实现了小于了,所以大于等于也很简单,使用逻辑运算符not搞定。

    @check_type
    def __ge__(self, other):
        return not self < other

通过对其他魔法方法的复用,可以很快完成魔法方法的定义,同时也减少了代码量。

日期加减运算

        这里的日期加减运算不是指日期和日期之间的加减。因为日期和日期之间的加减运算,没有什么实际意义。例如2023-8-1减去2023-1-1,我们说相差了7个月,但每个月的天数并不相同,还分平年和闰年。因此不能用年或月做单位,必须用天做单位得到的结果才准确。从较小的日期一天一天往后数,一直数到较大的日期,数了多少天这两个日期就相差多少天。所以我们需要实现日期和整型的加减运算,才能计算出两个日期之间相差了多少天。

__iadd__(加等于)

        这里为什么是使用__iadd__方法,而不是使用__add__方法呢?因为__add__方法涉及到深拷贝,两个对象在做加法的时候,不会改变对象本身,只会返回相加后的结果。但日期类的加法会改变属性__year、__month、__day的值,如果我们不想改变对象本身,就必须深拷贝当前对象,再把拷贝后的对象用来计算。而__iadd__方法呢,本来就是要改变对象的值的,所以我们先实现__iadd__,再通过__iadd__来实现__add__更好理解。

        我们现在来思考怎样实现__iadd__方法,当日期的__day属性加了数天后,我们需要判断此时的__day是否超出了当月天数的最大天数。如果超出了就需要向月进一,首先把当月的天数从__day中减去,再把月份加一。月份加一后又需要判断月份是否大于了12,如果大于了12就需要向年进一,首先把年份加一,再把月份重新赋值为1。判断一轮后再来判断__day是否大于当月的天数,直到__day小于等于当月的天数。如此我们定义出__iadd__方法:

    def __iadd__(self, other):
        self.__day += other  # __day加上数天
        while self.__day > self._month_day():  # 以__day大于当月天数作为循环条件
            self.__day -= self._month_day()  # __day减去当月天数
            self.__month += 1  # 月份加一
            if self.__month == 13:  # 判断月份是否等于13
                self.__month = 1  # 把月份重置为1
                self.__year += 1  # 把年份加一
        return self  # 返回自己(只要other不为0,这时属性__day、__month、__year或许已经改变)

实现了__iadd__方法后,我们就可以对MyDate的实例做+=运算了。

date1 = MyDate()
print(date1)
date1 += 200
print(date1)

执行结果如下:

执行结果中显示2023-1-1往后数200天就到了2023-7-20,我们可以手动验算一下结果是否正确。

        MyDate的实例可以和整型做+=运算了,但如果别人用MyDate的实例和浮点型做+=运算,就不符合我们的设计思路了。所以我们也可以用一个装饰器来判断other的类型是否为整型,是整型才做+=运算,不是整型就抛出错误。

from functools import wraps


class UpDateError(Exception):
    """修改日期错误"""

    def __str__(self):
        return "The right operand can only be an integer"


def check_int(func):
    """
    检查右操作数是否为整形的装饰器\n
    如果不是抛出 UpDateError\n
    :param func: 被装饰的函数
    :return:
    """
    @wraps(func)
    def check(*args, **kwargs):
        if type(args[1]) != int:
            raise UpDateError
        result = func(*args, **kwargs)
        return result
    return check

我们就可以把check_int装饰函数用于所有的加减运算方法上了。

__add__(加法)

        既然我们已经实现了__iadd__方法,那__add__就好写了。+=和+的区别就在于,+=会把相加结果重新赋值给对象,而+只会返回相加结果不会给对象重新赋值。所以我们只有深拷贝对象,再把深拷贝的对象做+=运算,最后返回深拷贝的对象,这样就能避免原对象被改变。于是我们利用copy中的deepcopy函数来实现__add__方法:

    @check_int
    def __add__(self, other):
        new = deepcopy(self)  # 深拷贝自己
        new += other  # 用深拷贝的对象来做+=运算
        return new  # 返回深拷贝对象

我们来验证一下做加法运算是否会改变原对象的值。

date1 = MyDate()
date2 = date1 + 200
print(date1)
print(date2)

执行结果如下:

我们可以发现做加法运算不会改变原对象的值。

__isub__(减等于)

        跟加法一样的原理,减法也是需要深拷贝的,所以我们先实现__isub__方法。怎样实现__isub__方法呢,当日期__day属性减去数天后,我们需要判断此时的__day是否为负数。如果为负数就需要向月借数,先把把月份减一,再把当月的天数加到__day中。月份减一后需要判断月份是否为0,如果月份为0就需要向年借数,先把把年份减一,再把月份重新赋值为12。年份减一后。需要判断年份是否小于0,如果小于0就要抛出错误。判断一轮后再来判断__day是否小于等于0,直到__day大于0。

    @check_int
    def __isub__(self, other):
        self.__day -= other
        while self.__day <= 0:
            self.__month -= 1
            if self.__month == 0:
                self.__year -= 1
                self.__month = 12
                if self.__year < 0:
                    raise ValueError('year cannot be less than zero')
            self.__day += self._month_day()
        return self

实现了__isub__方法后,MyDate的实例就可以做-=运算了。

date1 = MyDate()
date1 -= 100
print(date1)

执行结果如下:

执行结果中显示2023-1-1往前数100天就到了2022-9-23,我们可以手动验算一下结果是否正确。

__sub__(减法)

        实现了__isub__方法后,再实现__sub__方法就简单了。-=和-的区别就在于,-=会把相减结果重新赋值给对象,而-只会返回相减结果不会给对象重新赋值。所以我们只有深拷贝对象,再把深拷贝的对象做-=运算,最后返回深拷贝的对象,这样就能避免原对象被改变。于是我们利用copy中的deepcopy函数来实现__sub__方法:

    @check_int
    def __sub__(self, other):
        new = deepcopy(self)
        new -= other
        return new

我们来验证一下做减法运算是否会改变原对象的值。

date1 = MyDate()
date2 = date1 - 100
print(date1)
print(date2)

执行结果如下:

我们可以发现做减法运算不会改变原对象的值。

        虽然我们实现了日期加减天数的方法,但是这里面其实有一个BUG。就是天数为负数时,加减运算后的结果会有问题。

date1 = MyDate()
date2 = date1 + -200
date3 = date1 - -100
print(date1)
print(date2)
print(date3)

执行结果如下:

所以我们要在__iadd__和__isub__中增加处理加减负数的逻辑。加一个负数就相当于减一个正数,所以加一个负数要使用减法才对,我们就要在__iadd__中增加判断,当other小于0时用减法。减一个负数就相当于加一个正数,所以减一个负数要使用加法才对,我们就要在__isub__中增加判断,当other小于0时用加法。于是我们重新修改__iadd__和__isub__:

    @check_int
    def __iadd__(self, other):
        if other < 0:  # 增加判断
            return self - -other  # 返回减法结果
        self.__day += other
        while self.__day > self._month_day():
            self.__day -= self._month_day()
            self.__month += 1
            if self.__month == 13:
                self.__month = 1
                self.__year += 1
        return self

    @check_int
    def __isub__(self, other):
        if other < 0:  # 增加判断
            return self + -other  # 返回加法结果
        self.__day -= other
        while self.__day <= 0:
            self.__month -= 1
            if self.__month == 0:
                self.__year -= 1
                self.__month = 12
                if self.__year < 0:
                    raise ValueError('year cannot be less than zero')
            self.__day += self._month_day()
        return self

现在我们就能正确实现日期对负数的加减了。

date1 = MyDate()
date2 = date1 + -200
date3 = date1 - -100
print(date1)
print(date2)
print(date3)

 执行结果如下:

计算日期间隔

        我们还可以设计一个实例方法来计算两个日期之间的间隔,首先利用比较运算判断出两个日期的大小,然后再用小的日期不断加一看加多少个一才能和大的日期相等,我们就能得到两个日期间隔了多少天。为了避免改变日期的大小,还是要使用deepcopy深拷贝一下。

    @check_type
    def interval(self, other):
        day = 0
        if self < other:
            little, big = deepcopy(self), other  # self小时就深拷贝self
        elif self > other:
            little, big = deepcopy(other), self  # other小时就深拷贝other
        else:
            return day  # 相等时直接返回0
        while little < big:  # 以小的日期小于大的日期为循环条件
            little += 1  # 小的日期加一
            day += 1  # 天数加一
        return day  # 返回间隔天数

现在我们就能轻松得到两个日期间隔的天数了。

date1 = MyDate()
date2 = MyDate(2023, 8, 1)
print(date1.interval(date2))

执行结果如下:

得到当前日期

        我们还可以设计一个类方法来得到当前日期,至于为什么是类方法而不是实例方法?因为当前日期本来就是一个实例,通过实例来得到实例没有必要。直接通过类来得到实例,可以减少内存消耗,逻辑也清晰一点,而且实例也可以使用类方法。

        这里我们需要使用python中的time库来得到当前日期的年、月、日,再用类实例化出一个日期并返回。

    @classmethod
    def now(cls):
        time_list = time.strftime('%Y-%m-%d').split('-')
        return cls(int(time_list[0]), int(time_list[1]), int(time_list[2]))

我们就可以通过now方法得到当前日期的实例了。

date1 = MyDate.now()
print(date1)

执行结果如下:

格式化输出日期

        刚才在使用time中的strftime函数时,感觉strftime函数还挺好玩的。我们只要给它一个日期格式字符串,它就能给我们返回一个格式化的日期,而且这个格式还是我们自己设定的。所以我们可以在MyDate中也实现一个类似的方法,用来返回各种格式的日期字符串。

    def strfdate(self, format: str):
        """
        %Y  Year with century as a decimal number.\n
        %m  Month as a decimal number [01,12].\n
        %d  Day of the month as a decimal number [01,31].\n
        :param format: format string
        :return: format date
        """
        _month = f'{self.__month:0>2}'
        _day = f'{self.__day:0>2}'
        return format.replace('%Y', str(self.__year)).replace('%m', _month).replace('%d', _day)

我们就可以使用strfdate方法得到格式化日期了。

date = MyDate(2023, 2, 28)
print(date.strfdate('公元 %Y年 %m月 %d日'))
print(date.strfdate('年: %Y 月: %m 日: %d'))
print(date.strfdate('%Y-%m-%d'))

执行结果如下:

计算星期

        我们还可以设计一个实例方法来得到当前日期是星期几。刚好2023-1-1是星期日,我们就以2023-1-1为标志。首先用interval方法算出当前日期和2023-1-1的间隔天数,再用间隔天数对7做求余运算,得到的余数是几就是星期几。

    def week(self):
        _mark = MyDate(2023, 1, 1)
        _week = ['日', '一', '二', '三', '四', '五', '六']
        _day = self.interval(_mark)
        _day = _day % 7
        return '星期' + _week[_day]

来试一下week方法:

date1 = MyDate(2023, 2, 28)
print(date1.week())  # 星期二
date2 = MyDate(2022, 12, 31)
print(date2.week())  # 星期六

执行结果如下:

我们可以发现week有个BUG,当前日期大于2023-1-1时没有问题,当前日期小于2023-1-1时就有问题了。因为我们是正着数的从星期日——星期六,当前日期小于2023-1-1时应该倒着数从星期六——星期日。所以我们需要增加一个状态,当前日期大于2023-1-1时状态为1,当前日期小于2023-1-1时状态为-1。通过状态来决定最终从_week列表中取值时,是正着取还是倒着取。

    def week(self):
        _mark = MyDate(2023, 1, 1)
        _week = ['日', '一', '二', '三', '四', '五', '六']
        _day = self.interval(_mark)
        state = 1 if self >= _mark else -1
        _day = _day % 7
        return '星期' + _week[_day * state]

再来试一下week方法:

date1 = MyDate(2023, 2, 28)
print(date1.week())  # 星期二
date2 = MyDate(2022, 12, 31)
print(date2.week())  # 星期六

 执行结果如下:

迭代日期

        我们可以在MyDate中定义__iter__和__next__,使MyDate的实例成为迭代器,用来迭代日期。我们来设计一个迭代方式,让当前日期可以被迭代到当年的最后一天。为此我们需要在__init__中新增一个实例属性__next,用来作为终止迭代的条件,只要我们的年份到了下一年就终止迭代。

    def __init__(self, year=2023, month=1, day=1):
        self.__year = year
        self.__month = month
        self.__day = day
        if not (self.__year >= 0 and (0 < self.__month < 13) and (0 < self.__day <= self._month_day())):
            raise MyDateError(f'{self.__year}-{self.__month:0>2}-{self.__day:0>2}')
        self.__next = self.__year + 1

 __iter__(返回迭代器)

        在__iter__中我们需要返回一个迭代器,通常我们是返回自身(self),但如果你想迭代后对象的值不会被改变,就返回深拷贝的对象。在返回迭代器之前,先给__next属性赋值为下一年的年份。

    def __iter__(self):
        new = deepcopy(self)
        return new

 __next__(迭代)

        在__next__中我们需要设置迭代条件来迭代实例对象,并返回迭代值,迭代终止时抛出StopIteration错误。

    def __next__(self):
        self += 1  # 每次迭代日期递增1天
        if self.__year < self.__next:  # 判断年份是否到了下一年
            return self  # 返回迭代日期
        raise StopIteration  # 抛出StopIteration错误

 现在来试一下迭代日期:

date = MyDate.now()
for i in date:
    print(i)

我们就可以让MyDate的实例从当前日期迭代到当年最后一天了。 

        其实这里有个BUG,就是当我们对日期进行加减运算后,如果日期的年份变化了,迭代时就会出现BUG。因为__next属性没有跟随__year属性的变化而变化,这里有两种处理方式。一种在__iadd__方法和__isub__方法中添加__next属性的变更;另一种自定义__setattr__方法,在__year变更时同时变更__next。这里我选择了第一种方式::

    @check_int
    def __iadd__(self, other):
        """ Return self+=other. """
        if other < 0:
            return self - -other
        self.__day += other
        while self.__day > self._month_day():
            self.__day -= self._month_day()
            self.__month += 1
            if self.__month == 13:
                self.__month = 1
                self.__year += 1
                self.__next += 1  # 更改__next
        return self

    @check_int
    def __isub__(self, other):
        """ Return self-=other. """
        if other < 0:
            return self + -other
        self.__day -= other
        while self.__day <= 0:
            self.__month -= 1
            if self.__month == 0:
                self.__year -= 1
                self.__month = 12
                if self.__year < 0:
                    raise ValueError('year cannot be less than zero')
                self.__next -= 1  # 更改__next
            self.__day += self._month_day()
        return self

property描述符

        描述符是用来描述类属性的,只要一个类中实现了__get__、__set__、__delete__方法,那它的实例就能成为一个描述符。当一个类属性为描述符时,我们要访问它就会执行__get__、设置它的值就会执行__set__、删除它就会执行__delete__。因此我们可以在__get__、__set__、__delete__中设置各种条件逻辑,来达到控制类属性的目的。

        property时python内置的一种描述符,它是直接集成在python解释器中的,逻辑也是用C语言实现的。我们可以在MyDate中创建year、month、day三个类属性,并使用property来描述它们。这样做的目的是什么呢,就是假装告诉别人我有3个类属性year、month、day。你们可以通过这3个属性来得到年、月、日的值,设置年、月、日的值,删除年、月、日的值。但你要做的操作能不能成功,要看我允不允许你这样操作。但实际上年、月、日的值是在实例属性__year、__month、__day中的,类属性year、month、day就是个幌子,用来骗人的。想要访问、修改、设置__year、__month、__day的值,我们完全可以使用_MyDate__year、_MyDate__month、_MyDate__day来达到目的。看起来使用property这种操作是不是挺傻的,但是有很多类中都使用了property来骗人。但property也算是一种友善的提示吧,提示你不要乱改属性防止出现BUG,但你要真想改属性property也没法拦住你,除非它能描述实例属性。因此我们也可以在MyDate种使用property来提示别人不要乱改属性,顺带欺骗一下新手。

    def get_year(self):
        return self.__year

    def get_month(self):
        return self.__month

    def get_day(self):
        return self.__day

    year = property(lambda self: self.get_year(), lambda self, v: None, lambda self: None)  # default

    month = property(lambda self: self.get_month(), lambda self, v: None, lambda self: None)  # default

    day = property(lambda self: self.get_day(), lambda self, v: None, lambda self: None)  # default

property可以接收4个参数,第一个参数为访问属性时要执行的方法,这个方法只接收一个参数就是实例;第二个参数为设置属性时要执行的方法,这个方法要接收两个参数,实例和要设置的值;第三个参数为删除属性时要执行的方法,这个方法只接收一个参数,就是实例;第四个参数为描述字符串,没什么用一般不传。当然这里展示的只是property的一种用法,property还有另一种用法,就是作为装饰函数去装饰类中的方法,但同样不能真正的阻止你强行修改实例属性。好处是可以像使用属性一样使用方法,就是不用在方法名后加圆括号就能执行方法。

date = MyDate(2023, 2, 28)
date.year = 2020
del date.year
print(date.year)
print(date)

 执行结果如下:

从执行结果中我们可以发现,设置属性year无效,删除属性year无效,访问属性year有效。这就是因为使用property描述year后的效果,我们可以控制属性的访问、设置和删除,不过只能描述类属性,实例属性不能描述。

完整代码

import time
from copy import deepcopy
from functools import wraps
from typing import overload


class MyDateError(Exception):
    """非法日期错误"""

    def __init__(self, error_date):
        self.date = error_date

    def __str__(self):
        return f"The data should conform to the gregorian standards\nno exists date {self.date}"


class NoMyDateError(Exception):
    """不是MyDate类型错误"""

    def __str__(self):
        return "MyDate can only be compared to MyDate"


class UpDateError(Exception):
    """修改日期错误"""

    def __str__(self):
        return "The right operand can only be an integer"


def check_int(func):
    """
    检查右操作数是否为整形的装饰器\n
    如果不是抛出 UpDateError\n
    :param func: 被装饰的函数
    :return:
    """
    @wraps(func)
    def check(*args, **kwargs):
        if type(args[1]) != int:
            raise UpDateError
        result = func(*args, **kwargs)
        return result
    return check


def check_type(func):
    """
    检查比较的类型是否为MyDate的装饰器\n
    如果不是抛出 NoMyDateError\n
    :param func: 被装饰的函数
    :return:
    """
    @wraps(func)
    def check(*args, **kwargs):
        for _type in args:
            if type(_type) != MyDate:
                raise NoMyDateError
        result = func(*args, **kwargs)
        return result
    return check


class MyDate:
    """
    日期类
        def __init__(self, year=2023, month=1, day=1):
            self.__year = int(year)\n
            self.__month = int(month)\n
            self.__day = int(day)\n
    提供日期直接加减天数的操作方式
        MyDate += int or MyDate -= int\n
        MyDate + int or MyDate - int
    日期之间的比较
        MyDate == MyDate\n
        MyDate != MyDate\n
        MyDate < MyDate\n
        MyDate <= MyDate\n
        MyDate > MyDate\n
        MyDate >= MyDate\n
    now()方法得到当前日期\n
    interval(MyDate)方法计算当前日期和传入日期之间的间隔天数\n
    strfdate('%Y-%m-%d')方法可以格式化输出当前日期\n
    data()返回日期字符串: 年-月-日\n
    week()返回当前日期是星期几\n
    当前日期可迭代至当年第一天
    """
    __non_leap = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

    @overload
    def __init__(self, year=2023, month=1, day=1):
        ...

    @overload
    def __init__(self, year='2023', month='1', day='1'):
        ...

    def __init__(self, year=2023, month=1, day=1):
        self.__year = int(year)
        self.__month = int(month)
        self.__day = int(day)
        if not (self.__year >= 0 and (0 < self.__month < 13) and (0 < self.__day <= self._month_day())):
            raise MyDateError(f'{self.__year}-{self.__month:0>2}-{self.__day:0>2}')
        self.__next = self.__year + 1

    def get_year(self):
        return self.__year

    def get_month(self):
        return self.__month

    def get_day(self):
        return self.__day

    @classmethod
    def now(cls):
        """返回当前日期实例"""
        time_list = time.strftime('%Y-%m-%d').split('-')
        return cls(int(time_list[0]), int(time_list[1]), int(time_list[2]))

    def strfdate(self, format: str):
        """
        %Y  Year with century as a decimal number.\n
        %m  Month as a decimal number [01,12].\n
        %d  Day of the month as a decimal number [01,31].\n
        :param format: format string
        :return: format date
        """
        _month = f'{self.__month:0>2}'
        _day = f'{self.__day:0>2}'
        return format.replace('%Y', str(self.__year)).replace('%m', _month).replace('%d', _day)

    @check_type
    def interval(self, other):
        """
        返回当前日期和另一个日期的间隔天数\n
        :param other: 另一个日期
        :return: 间隔天数
        """
        day = 0
        if self < other:
            little, big = deepcopy(self), other
        elif self > other:
            little, big = deepcopy(other), self
        else:
            return day
        while little < big:
            little += 1
            day += 1
        return day

    def date(self):
        """返回日期字符串"""
        return f'{self.__year}-{self.__month:0>2}-{self.__day:0>2}'

    def week(self):
        """返回当前日期是星期几"""
        _mark = MyDate(2023, 1, 1)
        _week = ['日', '一', '二', '三', '四', '五', '六']
        _day = self.interval(_mark)
        state = 1 if self >= _mark else -1
        _day = _day % 7
        return '星期' + _week[_day * state]

    def _month_day(self):
        """ Return how many days are there in the same month. """
        if self.__month == 2 and ((self.__year % 4 == 0 and self.__year % 100 != 0) or (self.__year % 400 == 0)):
            return 29
        return self.__non_leap[self.__month]

    @check_int
    def __iadd__(self, other):
        """ Return self+=other. """
        if other < 0:
            return self - -other
        self.__day += other
        while self.__day > self._month_day():
            self.__day -= self._month_day()
            self.__month += 1
            if self.__month == 13:
                self.__month = 1
                self.__year += 1
                self.__next += 1
        return self

    @check_int
    def __add__(self, other):
        """ Return self+other. """
        new = deepcopy(self)
        new += other
        return new

    @check_int
    def __isub__(self, other):
        """ Return self-=other. """
        if other < 0:
            return self + -other
        self.__day -= other
        while self.__day <= 0:
            self.__month -= 1
            if self.__month == 0:
                self.__year -= 1
                self.__month = 12
                if self.__year < 0:
                    raise ValueError('year cannot be less than zero')
                self.__next -= 1
            self.__day += self._month_day()
        return self

    @check_int
    def __sub__(self, other):
        """ Return self-other. """
        new = deepcopy(self)
        new -= other
        return new

    @check_type
    def __eq__(self, other):
        """ Return self==other. """
        return self.__year == other.year and self.__month == other.month and self.__day == other.day

    @check_type
    def __ne__(self, other):
        """ Return self!=other. """
        return not self == other

    @check_type
    def __lt__(self, other):
        """ Return self<other. """
        if self.__year < other.year:
            return True
        elif self.__year == other.year and self.__month < other.month:
            return True
        elif self.__year == other.year and self.__month == other.month and self.__day < other.day:
            return True
        return False

    @check_type
    def __gt__(self, other):
        """ Return self>other. """
        return not self <= other

    @check_type
    def __le__(self, other):
        """ Return self<=other. """
        return self < other or self == other

    @check_type
    def __ge__(self, other):
        """ Return self>=other. """
        return not self < other

    def __iter__(self):
        """ Return new -> deepcopy(self). """
        new = deepcopy(self)
        return new

    def __next__(self):
        """ Iterator self. """
        self += 1
        if self.__year < self.__next:
            return self
        raise StopIteration

    def __str__(self):
        """ Return self string. """
        return f'{self.__year}-{self.__month:0>2}-{self.__day:0>2}'

    year = property(lambda self: self.get_year(), lambda self, v: None, lambda self: None)  # default

    month = property(lambda self: self.get_month(), lambda self, v: None, lambda self: None)  # default

    day = property(lambda self: self.get_day(), lambda self, v: None, lambda self: None)  # default

如果你还有什么好的想法,可以自己在MyDate中实现出来。

猜你喜欢

转载自blog.csdn.net/qq_40148262/article/details/132191795