Python customizes a date class

        Through our previous understanding of magic methods in python, now we use these magic methods to customize a date class to deepen our understanding and application of magic methods.

Date class

        First, let's write an __init__ method to initialize the date class. In __init__ we can use default parameters to prevent the situation where we forget to pass parameters during instantiation and cannot be instantiated. The year, month, and day attributes in the date class can be hidden using double underscores to prevent others from modifying the attribute values ​​​​at will.

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

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

 Now we can get the date object by instantiating MyDate, and just pass in some parameters to get the date object.

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

 The execution results are as follows:

We found that the object information printed out by print is not good-looking. It would be nice if we could print out <year-month-day>. So we thought of the __str__ method. Print calls the __str__ method to print object information. We can solve this problem by customizing the __str__ method, so we wrote the __str__ method in the MyDate class. In the __str__ method, we need to return a string containing year, month, and day information. We can do some processing for the month and day, so that they remain two digits, and if they are less than two digits, add 0 in front of them (for example: July 06).

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

After we add the __str__ method, we run the program again and print out the object information.

Doesn't it look more comfortable now?

Judgment date

        Although we have implemented the creation of date objects, there is a BUG here. We can create any time, such as January 60, 2023.

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

 The execution results are as follows:

A date like January 60, 2023 simply does not exist and does not comply with the rules of dates. Therefore, we need to make a judgment on the date to determine whether the date meets the Gregorian calendar standard. We will create a date object only if it meets the Gregorian calendar standard. If it does not meet the Gregorian calendar standard, an error will be thrown.

        Our Gregorian calendar date year must be greater than or equal to 0, the month is 1~12, and the number of days in each month is determined based on the year and month. We can find that the year and month are easy to judge, but the number of days in each month is difficult to judge, but don’t be afraid as we analyze it slowly. If the year is divided into ordinary years and leap years, February has 28 days in ordinary years and February 29 in leap years. The other months are the same, January 31 days, March 31 days, April 30 days, May 31 days, June 30 days, July 31 days, August 31 days, September 30 days, October 31 days, 30 days in November and 30 days in December. Based on the number of days in the month, we can design a month list, and put the number of days in each month into the corresponding position of the list index. We can get a list of months in ordinary years and a list of months in leap years.

    __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]  # 闰年月份列表

Because the list index starts from 0, and we don't have the month 0, the first element is occupied by 0, and we can't use the first element. The month list is also hidden using double underlines to prevent it from being modified at will.

        Then we have to distinguish between ordinary years and leap years. For ordinary years, use the ordinary year list, and for leap years, use the leap year list. So how to distinguish between ordinary years and leap years? Years that are divisible by 4 but not 100, or divisible by 400, are called leap years. Except for leap years, the rest are ordinary years. Therefore, we can only judge leap years. If it is a leap year, use the leap year month list, otherwise use the ordinary year month list. So we wrote a method in the MyDate class to return the number of days in the month, as follows:

    __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]  # 返回平年月份的天数

After writing this method, we found that it is actually a bit redundant. There is only one day difference between the list of months in ordinary years and the list of months in leap years. We can only use one list. We add another condition to the if judgment condition, whether the current month is February. If the month is February and the year is a leap year, 29 days will be returned. Otherwise, the number of days in the corresponding month in the month list of ordinary years will be returned. So we modified the _month_day() method and changed it to the following:

    __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]  # 返回平年月份的天数

With this function to determine the number of days in the month, we can determine whether the date meets the Gregorian calendar standard during initialization. So we add a logical statement to determine the date in the __init__ method:

    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

Now we can use the MyDate class to instantiate the correct date. If the date is incorrect, the instantiation will fail.

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

The execution results are as follows:

Although errors can now be thrown, the valueError thrown does not match our MyDate type very well. Moreover, it just throws ValueError without any text description, which also makes people confused as to why the error is reported. So we can customize the error type and throw an error that matches the MyDate type.

Custom date error type

        The error types in python are basically inherited Exception, and our custom errors also inherit Exception. We can pass the error date to the instance of the error type. In this case, we need to customize the __init__ method to receive the error date and assign the error date to the instance attribute; then customize the __str__ method to describe the error content. And add the error date to the description. So we can write the following error types:

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}"

        Now we change the __init__ method from throwing ValueError to throwing 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}')

Let's instantiate the error date again and check the error report.

Isn’t this way of reporting errors much better?

Date size comparison

        The prototype of MyDate has come out, and now we can give it the ability to compare sizes between similar ones. So we came up with magic methods for comparison operations: __eq__, __ne__, __lt__, __le__, __gt__, __ge__. As long as we implement these magic methods in MyDate, we can let instances of MyDate perform comparison operations.

__eq__ (equal to)

        First we need to think about whether the two dates are equal and what needs to be compared. It should be necessary to check whether the three attributes of year, month and day are equal. If the year, month and day are equal, then we can say that the two dates are equal. So we define the __eq__ method in the class.

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

Let's try to see if the MyDate instance can perform equal operations.

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

The execution results are as follows:

We can see that we can correctly determine whether two dates are equal, but what if we use other types to compare with MyDate?

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

The execution results are as follows:

We can see that an error was reported. Not only was the error reported, but our hidden attribute _MyDate__year was also exposed. So in order to avoid this kind of error, we need to determine whether the type of parameter other is MyDate before comparing. If it is, we will compare, if not, we will directly throw an error. So we should add logic to determine whether other is of type MyDate in the __eq__ method, but don't panic yet. Think about it carefully, since the type needs to be judged for equals, it does not mean that the type must be judged. Less than, greater than, less than or equal to, and greater than or equal to should also judge the type. Since all comparison methods need to determine the type, we must write a code to determine the type in each comparison method. Doing this will make our code redundant. Is there any way to reduce code redundancy? So we thought of decorators, which can be used to determine types and reduce code duplication. We wrote the following decoration function:

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

When an error is thrown in the decorated function, an error is also defined. Now that the decoration function is written, let’s decorate it into the comparison method.

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

Now let's compare it with other types to see how the error is displayed.

Now this kind of error reporting is much better, and our hidden attributes will no longer be exposed.

__ne__ (not equal to)

        Now that we have achieved equality, not equal is simple. Because the opposite of equal is not equal, it can be easily done using the logical operator not.

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

We perform the == operation in __ne__, and then invert the operation result to get the operation result of !=.

__lt__ (less than)

        First, let's think about how to judge the size of two dates. It should be that the year is smaller than the smaller date; when the year is equal, the month is smaller, and the month is smaller than the smaller date; when the year and month are both equal, the days are compared, and the date with fewer days is smaller. So we define the __lt__ method in MyDate:

    @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__ (less than or equal to)

        We have already implemented the equal to and less than methods, so less than or equal to is simple. Because less than or equal to is less than or equal to, just use the logical operator or to process the results of less than and equal to.

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

__gt__ (greater than)

        Less than or equal to is realized, and greater than is simple. It is the opposite of less than or equal to, and it can be done by using the logical operator not.

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

__ge__ (greater than or equal to)

        Greater than or equal to is the opposite of less than. We have already implemented less than, so greater than or equal to is also very simple. Use the logical operator not to get it.

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

By reusing other magic methods, the definition of magic methods can be completed quickly, while also reducing the amount of code.

Date addition and subtraction

        The date addition and subtraction operations here do not refer to the addition and subtraction between dates. Because the addition and subtraction between dates has no practical meaning. For example, 2023-8-1 minus 2023-1-1, we say there is a difference of 7 months, but the number of days in each month is different, and it is also divided into ordinary years and leap years. Therefore, you cannot use years or months as units. You must use days as units to get accurate results. Count from the smaller date day by day until you count to the larger date. The difference between the two dates will be as many days as you count. So we need to implement the addition and subtraction of dates and integers to calculate the number of days between two dates.

__iadd__ (add equals)

        Why is the __iadd__ method used here instead of the __add__ method? Because the __add__ method involves deep copying, when two objects are added, the object itself will not be changed, and only the added result will be returned. However, the addition of the date class will change the values ​​of the attributes __year, __month, and __day. If we do not want to change the object itself, we must deeply copy the current object and then use the copied object for calculation. The __iadd__ method is originally intended to change the value of the object, so we implement __iadd__ first, and then implement __add__ through __iadd__ for better understanding.

        Let's now think about how to implement the __iadd__ method. When a few days are added to the __day attribute of the date, we need to determine whether the __day at this time exceeds the maximum number of days in the month. If it exceeds, you need to add one to the month. First, subtract the number of days in the current month from __day, and then add one to the month. After adding one to the month, you need to determine whether the month is greater than 12. If it is greater than 12, you need to add one to the year. First, add one to the year, and then reassign the month to 1. After one round of judgment, judge whether __day is greater than the number of days in the current month, until __day is less than or equal to the number of days in the current month. So we define the __iadd__ method:

    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或许已经改变)

After implementing the __iadd__ method, we can perform += operations on instances of MyDate.

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

The execution results are as follows:

The execution result shows that 200 days from 2023-1-1 will arrive at 2023-7-20. We can manually check whether the result is correct.

        Instances of MyDate can perform += operations with integers, but if others use instances of MyDate to perform += operations with floating-point types, it will not conform to our design ideas. So we can also use a decorator to determine whether the type of other is an integer. If it is an integer, the += operation will be performed. If it is not an integer, an error will be thrown.

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

We can use the check_int decorative function for all addition and subtraction operations.

__add__ (addition)

        Now that we have implemented the __iadd__ method, __add__ is easy to write. The difference between += and + is that += will reassign the addition result to the object, while + will only return the addition result and will not reassign the object. So we only have a deep copy of the object, then perform the += operation on the deep copy object, and finally return the deep copy object, thus preventing the original object from being changed. So we use the deepcopy function in copy to implement the __add__ method:

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

Let's verify whether the addition operation will change the value of the original object.

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

The execution results are as follows:

We can find that the addition operation will not change the value of the original object.

__isub__ (minus equal to)

        The same principle as addition, subtraction also requires deep copying, so we first implement the __isub__ method. How to implement the __isub__ method? When a number of days are subtracted from the date __day attribute, we need to determine whether the __day at this time is a negative number. If it is a negative number, you need to borrow the number from the month. First subtract one from the month, and then add the number of days in the current month to __day. After subtracting one from the month, you need to determine whether the month is 0. If the month is 0, you need to borrow a number from the year. First, subtract one from the year, and then reassign the month to 12. Year minus one. It is necessary to determine whether the year is less than 0. If it is less than 0, an error will be thrown. After one round of judgment, judge whether __day is less than or equal to 0 until __day is greater than 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

After implementing the __isub__ method, instances of MyDate can perform the -= operation.

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

The execution results are as follows:

The execution result shows that 100 days from 2023-1-1 to 2022-9-23, we can manually check whether the result is correct.

__sub__ (subtraction)

        After implementing the __isub__ method, it is simple to implement the __sub__ method. The difference between -= and - is that -= will reassign the subtraction result to the object, while - will only return the subtraction result and not reassign the object. So we only have a deep copy of the object, then perform the -= operation on the deep copy object, and finally return the deep copy object, thus preventing the original object from being changed. So we use the deepcopy function in copy to implement the __sub__ method:

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

Let's verify whether doing the subtraction operation will change the value of the original object.

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

The execution results are as follows:

We can find that doing subtraction will not change the value of the original object.

        Although we have implemented the method of adding and subtracting days to a date, there is actually a BUG in it. That is, when the number of days is a negative number, there will be problems with the results after addition and subtraction.

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

The execution results are as follows:

So we need to add logic for processing addition and subtraction of negative numbers in __iadd__ and __isub__. Adding a negative number is equivalent to subtracting a positive number, so subtraction is the only way to add a negative number. We need to add a judgment in __iadd__ and use subtraction when other is less than 0. Subtracting a negative number is equivalent to adding a positive number, so addition is the only way to subtract a negative number. We need to add a judgment in __isub__, and use addition when other is less than 0. So we re-modify __iadd__ and __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

Now we can correctly add and subtract negative numbers to dates.

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

 The execution results are as follows:

Calculate date interval

        We can also design an instance method to calculate the interval between two dates. First, use a comparison operation to determine the size of the two dates, and then continue to add the smaller date to see how many ones can be added to be equal to the larger date. We can get the number of days between two dates. In order to avoid changing the date size, you still need to use deepcopy to make a deep copy.

    @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  # 返回间隔天数

Now we can easily get the number of days between two dates.

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

The execution results are as follows:

get current date

        We can also design a class method to get the current date. As for why it is a class method instead of an instance method? Because the current date is an instance, it is not necessary to get the instance through the instance. Obtaining instances directly through classes can reduce memory consumption and make the logic clearer, and instances can also use class methods.

        Here we need to use the time library in python to get the year, month, and day of the current date, and then use the class to instantiate a date and return it.

    @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]))

We can get the instance of the current date through the now method.

date1 = MyDate.now()
print(date1)

The execution results are as follows:

Format output date

        When I was using the strftime function in time just now, I felt that the strftime function was quite fun. We only need to give it a date format string, and it will return us a formatted date, and this format is set by ourselves. So we can implement a similar method in MyDate to return date strings in various formats.

    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)

We can use the strfdate method to get the formatted date.

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

The execution results are as follows:

Calculate the day of the week

        We can also design an instance method to get the day of the week the current date is. It happens that 2023-1-1 is Sunday, so we use 2023-1-1 as the mark. First, use the interval method to calculate the number of days between the current date and 2023-1-1, and then use the interval number to calculate the remainder of 7. The resulting remainder is the day of the week.

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

Let’s try the week method:

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

The execution results are as follows:

We can find that there is a BUG in week. There is no problem when the current date is greater than 2023-1-1, but there is a problem when the current date is less than 2023-1-1. Because we are counting forward from Sunday to Saturday, when the current date is less than 2023-1-1, we should count backward from Saturday to Sunday. So we need to add a status. When the current date is greater than 2023-1-1, the status is 1. When the current date is less than 2023-1-1, the status is -1. The status is used to determine whether the final value from the _week list is taken forward or backward.

    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]

Let’s try the week method again:

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

 The execution results are as follows:

iteration date

        We can define __iter__ and __next__ in MyDate to make the MyDate instance an iterator for iterating dates. Let's design an iteration method so that the current date can be iterated to the last day of the year. For this reason, we need to add an instance attribute __next in __init__, which is used as a condition to terminate the iteration. As long as our year reaches the next year, the iteration will be terminated.

    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__ (returns the iterator)

        In __iter__ we need to return an iterator. Usually we return self (self), but if you want the value of the object not to be changed after iteration, return a deep copy of the object. Before returning the iterator, assign the __next attribute to the next year.

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

 __next__ (iteration)

        In __next__ we need to set the iteration conditions to iterate the instance object and return the iteration value. A StopIteration error is thrown when the iteration terminates.

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

 Now let's try iterating over dates:

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

We can let the MyDate instance iterate from the current date to the last day of the year. 

        In fact, there is a BUG here, that is, when we add and subtract dates, if the year of the date changes, a BUG will occur during iteration. Because the __next attribute does not change with the change of the __year attribute, there are two ways to deal with it. One is to add changes to the __next attribute in the __iadd__ method and __isub__ method; the other is to customize the __setattr__ method to change __next at the same time when __year changes. Here I chose the first way::

    @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 descriptor

        Descriptors are used to describe class attributes. As long as a class implements the __get__, __set__, and __delete__ methods, its instance can become a descriptor. When a class attribute is a descriptor, we will execute __get__ to access it, __set__ to set its value, and __delete__ to delete it. Therefore, we can set various conditional logic in __get__, __set__, and __delete__ to achieve the purpose of controlling class attributes.

        Property is a descriptor built into python. It is directly integrated into the python interpreter, and the logic is also implemented in C language. We can create three class attributes of year, month, and day in MyDate and use property to describe them. What is the purpose of doing this? It is to pretend to tell others that I have three class attributes: year, month, and day. You can use these three attributes to get the year, month, and day values, set the year, month, and day values, and delete the year, month, and day values. But whether the operation you want to do can be successful depends on whether I allow you to do this. But in fact, the values ​​of year, month, and day are in the instance attributes __year, __month, and __day. The class attributes year, month, and day are just a cover to deceive people. If we want to access, modify, and set the values ​​of __year, __month, and __day, we can completely use _MyDate__year, _MyDate__month, and _MyDate__day to achieve our goals. It seems silly to use properties, but there are many classes that use properties to deceive people. But property can be regarded as a friendly reminder, reminding you not to change properties randomly to prevent bugs, but if you really want to change properties, property cannot stop you unless it can describe instance properties. Therefore, we can also use property in MyDate to remind others not to change properties randomly and deceive novices by the way.

    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 can receive 4 parameters. The first parameter is the method to be executed when accessing the property. This method only receives one parameter, which is the instance; the second parameter is the method to be executed when setting the property. This method needs to receive two parameters. The instance and the value to be set; the third parameter is the method to be executed when deleting the attribute. This method only receives one parameter, which is the instance; the fourth parameter is the description string, which is useless and generally not passed. Of course, what is shown here is only one usage of property. There is another usage of property, which is to use it as a decoration function to decorate methods in a class, but it also does not really prevent you from forcibly modifying instance properties. The advantage is that you can use methods just like attributes, that is, you can execute methods without adding parentheses after the method name.

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

 The execution results are as follows:

From the execution results, we can find that setting the attribute year is invalid, deleting the attribute year is invalid, and accessing the attribute year is valid. This is because using property to describe the effect of year, we can control the access, setting and deletion of properties, but only class properties can be described, not instance properties.

Complete code

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

If you have any good ideas, you can implement them yourself in MyDate.

Guess you like

Origin blog.csdn.net/qq_40148262/article/details/132191795