开发手册 欢迎您!
软件开发者资料库

Python 中3.6及之前版本datetime没有fromisoformat()的解决方法

Python 中可以使用datetime的fromisoformat()方法将字符串转成datetime对象,但只有Python 3.7以上才支持,Python 3.6及之前的版本不支持,调用会报错:AttributeError: type object 'datetime.datetime' has no attribute 'fromisoformat'。本文主要介绍Python 3.6及之前的版本的兼容解决方法。

1、使用datetime.strptime()转换

from datetime import datetimedt = datetime.now()iso_datetime_string = dt.isoformat()# >>> '2019-10-20T15:54:53.840416'datetime.strptime(iso_datetime_string,"%Y-%m-%dT%H:%M:%S.%f")# >>> datetime.datetime(2019, 10, 20, 15, 54, 53, 840416)

2、实现fromisoformat()方法

fromisoformat()函数用于从包含 ISO 格式日期的指定字符串构造日期对象。Python 3.7中可以直接使用,之前版本可以参考实现,代码如下:

from datetime import timedelta, time, date, timezone, tzinfofrom datetime import datetime as system_datetimeimport time as _timeimport math as _mathclass datetime(system_datetime):    def __init__(self, *a, **k):        self._fold = 1  # idk what this does    @classmethod    def fromisoformat(cls, date_string):        """        IPCP FW uses python 3.5 (as of Jan 2022) which does not have some helpful methods like .fromisoformat().        This adds some functionality that is found in python 3.8        Copied this directly (mostly) from the python 3.8 datetime module.        Construct a datetime from the output of datetime.isoformat()."""        if not isinstance(date_string, str):            raise TypeError('fromisoformat: argument must be str')        # Split this at the separator        dstr = date_string[0:10]        tstr = date_string[11:]        try:            date_components = _parse_isoformat_date(dstr)        except ValueError:            raise ValueError('Invalid isoformat string: {}'.format(date_string))        if tstr:            try:                time_components = _parse_isoformat_time(tstr)            except ValueError:                raise ValueError('Invalid isoformat string: {date_string}'.format(date_string))        else:            time_components = [0, 0, 0, 0, None]        return cls(*(date_components + time_components))    def astimezone(self, tz=None):        # converts to local timezone if tz=None        if tz is None:            tz = self._local_timezone()        elif not isinstance(tz, tzinfo):            raise TypeError("tz argument must be an instance of tzinfo")        mytz = self.tzinfo        if mytz is None:            mytz = self._local_timezone()            myoffset = mytz.utcoffset(self)        else:            myoffset = mytz.utcoffset(self)            if myoffset is None:                mytz = self.replace(tzinfo=None)._local_timezone()                myoffset = mytz.utcoffset(self)        if tz is mytz:            return self        # Convert self to UTC, and attach the new time zone object.        utc = (self - myoffset).replace(tzinfo=tz)        # Convert from UTC to tz's local time.        return tz.fromutc(utc)    def _local_timezone(self):        if self.tzinfo is None:            ts = self._mktime()        else:            ts = (self - _EPOCH) // timedelta(seconds=1)        localtm = _time.localtime(ts)        local = datetime(*localtm[:6])        # Extract TZ data        gmtoff = localtm.tm_gmtoff        zone = localtm.tm_zone        return timezone(timedelta(seconds=gmtoff), zone)    def _mktime(self):        """Return integer POSIX timestamp."""        epoch = datetime(1970, 1, 1)        max_fold_seconds = 24 * 3600        t = (self - epoch) // timedelta(0, 1)        def local(u):            y, m, d, hh, mm, ss = _time.localtime(u)[:6]            return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1)        # Our goal is to solve t = local(u) for u.        a = local(t) - t        u1 = t - a        t1 = local(u1)        if t1 == t:            # We found one solution, but it may not be the one we need.            # Look for an earlier solution (if `fold` is 0), or a            # later one (if `fold` is 1).            u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self.fold]            b = local(u2) - u2            if a == b:                return u1        else:            b = t1 - u1            assert a != b        u2 = t - b        t2 = local(u2)        if t2 == t:            return u2        if t1 == t:            return u1        # We have found both offsets a and b, but neither t - a nor t - b is        # a solution.  This means t is in the gap.        return (max, min)[self.fold](u1, u2)    def setstate(self, string, tzinfo):        if tzinfo is not None and not isinstance(tzinfo):            raise TypeError("bad tzinfo state arg")        (yhi, ylo, m, self._day, self._hour,         self._minute, self._second, us1, us2, us3) = string        if m > 127:            self._fold = 1            self._month = m - 128        else:            self._fold = 0            self._month = m        self._year = yhi * 256 + ylo        self._microsecond = (((us1 << 8) | us2) << 8) | us3        self._tzinfo = tzinfo    @property    def fold(self):        return self._fold_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)MINYEAR = 1MAXYEAR = 9999_MAXORDINAL = 3652059  # date.max.toordinal()# Helpers for parsing the result of isoformat()def _parse_isoformat_date(dtstr):    # It is assumed that this function will only be called with a    # string of length exactly 10, and (though this is not used) ASCII-only    year = int(dtstr[0:4])    if dtstr[4] != '-':        raise ValueError('Invalid date separator: %s' % dtstr[4])    month = int(dtstr[5:7])    if dtstr[7] != '-':        raise ValueError('Invalid date separator')    day = int(dtstr[8:10])    return [year, month, day]def _parse_hh_mm_ss_ff(tstr):    # Parses things of the form HH[:MM[:SS[.fff[fff]]]]    len_str = len(tstr)    time_comps = [0, 0, 0, 0]    pos = 0    for comp in range(0, 3):        if (len_str - pos) < 2:            raise ValueError('Incomplete time component')        time_comps[comp] = int(tstr[pos:pos + 2])        pos += 2        next_char = tstr[pos:pos + 1]        if not next_char or comp >= 2:            break        if next_char != ':':            raise ValueError('Invalid time separator: %c' % next_char)        pos += 1    if pos < len_str:        if tstr[pos] != '.':            raise ValueError('Invalid microsecond component')        else:            pos += 1            len_remainder = len_str - pos            if len_remainder not in (3, 6):                raise ValueError('Invalid microsecond component')            time_comps[3] = int(tstr[pos:])            if len_remainder == 3:                time_comps[3] *= 1000    return time_compsdef _parse_isoformat_time(tstr):    # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]    len_str = len(tstr)    if len_str < 2:        raise ValueError('Isoformat time too short')    # This is equivalent to re.search('[+-]', tstr), but faster    tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1)    timestr = tstr[:tz_pos - 1] if tz_pos > 0 else tstr    time_comps = _parse_hh_mm_ss_ff(timestr)    tzi = None    if tz_pos > 0:        tzstr = tstr[tz_pos:]        # Valid time zone strings are:        # HH:MM               len: 5        # HH:MM:SS            len: 8        # HH:MM:SS.ffffff     len: 15        if len(tzstr) not in (5, 8, 15):            raise ValueError('Malformed time zone string')        tz_comps = _parse_hh_mm_ss_ff(tzstr)        if all(x == 0 for x in tz_comps):            tzi = timezone.utc        else:            tzsign = -1 if tstr[tz_pos - 1] == '-' else 1            td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],                           seconds=tz_comps[2], microseconds=tz_comps[3])            tzi = timezone(tzsign * td)    time_comps.append(tzi)    return time_compsdef _check_utc_offset(name, offset):    assert name in ("utcoffset", "dst")    if offset is None:        return    if not isinstance(offset, timedelta):        raise TypeError("tzinfo.%s() must return None "                        "or timedelta, not '%s'" % (name, type(offset)))    if not -timedelta(1) < offset < timedelta(1):        raise ValueError("%s()=%s, must be strictly between "                         "-timedelta(hours=24) and timedelta(hours=24)" %                         (name, offset))def _check_int_field(value):    if isinstance(value, int):        return value    if isinstance(value, float):        raise TypeError('integer argument expected, got float')    try:        value = value.__index__()    except AttributeError:        pass    else:        if not isinstance(value, int):            raise TypeError('__index__ returned non-int (type %s)' %                            type(value).__name__)        return value    orig = value    try:        value = value.__int__()    except AttributeError:        pass    else:        if not isinstance(value, int):            raise TypeError('__int__ returned non-int (type %s)' %                            type(value).__name__)        import warnings        warnings.warn("an integer is required (got type %s)" %                      type(orig).__name__,                      DeprecationWarning,                      stacklevel=2)        return value    raise TypeError('an integer is required (got type %s)' %                    type(value).__name__)def _check_date_fields(year, month, day):    year = _check_int_field(year)    month = _check_int_field(month)    day = _check_int_field(day)    if not MINYEAR <= year <= MAXYEAR:        raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year)    if not 1 <= month <= 12:        raise ValueError('month must be in 1..12', month)    dim = _days_in_month(year, month)    if not 1 <= day <= dim:        raise ValueError('day must be in 1..%d' % dim, day)    return year, month, daydef _check_time_fields(hour, minute, second, microsecond, fold):    hour = _check_int_field(hour)    minute = _check_int_field(minute)    second = _check_int_field(second)    microsecond = _check_int_field(microsecond)    if not 0 <= hour <= 23:        raise ValueError('hour must be in 0..23', hour)    if not 0 <= minute <= 59:        raise ValueError('minute must be in 0..59', minute)    if not 0 <= second <= 59:        raise ValueError('second must be in 0..59', second)    if not 0 <= microsecond <= 999999:        raise ValueError('microsecond must be in 0..999999', microsecond)    if fold not in (0, 1):        raise ValueError('fold must be either 0 or 1', fold)    return hour, minute, second, microsecond, folddef _check_tzinfo_arg(tz):    if tz is not None and not isinstance(tz, tzinfo):        raise TypeError("tzinfo argument must be None or of a tzinfo subclass")def _cmperror(x, y):    raise TypeError("can't compare '%s' to '%s'" % (        type(x).__name__, type(y).__name__))def _divide_and_round(a, b):    """divide a by b and round result to the nearest integer    When the ratio is exactly half-way between two integers,    the even integer is returned.    """    # Based on the reference implementation for divmod_near    # in Objects/longobject.c.    q, r = divmod(a, b)    # round up if either r / b > 0.5, or r / b == 0.5 and q is odd.    # The expression r / b > 0.5 is equivalent to 2 * r > b if b is    # positive, 2 * r < b if b negative.    r *= 2    greater_than_half = r > b if b > 0 else r < b    if greater_than_half or r == b and q % 2 == 1:        q += 1    return qdef _days_in_month(year, month):    "year, month -> number of days in that month in that year."    assert 1 <= month <= 12, month    if month == 2 and _is_leap(year):        return 29    return _DAYS_IN_MONTH[month]_DAYS_IN_MONTH = [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]def _is_leap(year):    "year -> 1 if leap year, else 0."    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

参考文档:https://github.com/wonhero/gs_datetime/blob/master/gs_datetime.py