functools — Функции высшего порядка и операции над вызываемыми объектами
Исходный код: Lib/functools.py
Модуль functools предназначен для функций высшего порядка: функций, которые работают с другими функциями или возвращают их. В общем случае любой вызываемый объект может рассматриваться как функция для целей этого модуля.
Модуль functools определяет следующие функции:
-
@functools.cache(user_function) -
Простой и лёгкий кэш функций без ограничения размера. Иногда его называют «мемоизацией».
Возвращает то же, что и
lru_cache(maxsize=None), создавая тонкую обёртку, которая выполняет поиск аргументов функции в словаре. Поскольку старые значения никогда не требуется удалять, этот вариант меньше и быстрее, чемlru_cache()с ограничением размера.Например:
@cache def factorial(n): return n * factorial(n-1) if n else 1 >>> factorial(10) # no previously cached result, makes 11 recursive calls 3628800 >>> factorial(5) # just looks up cached value result 120 >>> factorial(12) # makes two new recursive calls, the other 10 are cached 479001600Кэш потокобезопасен, поэтому обёрнутую функцию можно использовать в нескольких потоках. Это означает, что базовая структура данных сохранит целостность при одновременных обновлениях.
Обёрнутая функция может быть вызвана несколько раз, если другой поток выполнит дополнительный вызов до завершения и кэширования первоначального вызова.
Добавлено в версии 3.9.
-
@functools.cached_property(func) -
Преобразует метод класса в свойство, значение которого вычисляется один раз, а затем кэшируется как обычный атрибут на всё время существования экземпляра. Аналогично
property(), но с кэшированием. Полезно для дорогостоящих вычисляемых свойств экземпляров, которые в остальном фактически неизменяемы.Пример:
class DataSet: def __init__(self, sequence_of_numbers): self._data = tuple(sequence_of_numbers) @cached_property def stdev(self): return statistics.stdev(self._data)Механизм работы
cached_property()несколько отличается отproperty(). Обычное свойство запрещает запись атрибута, если не определён метод-сеттер. В отличие от него, cached_property допускает запись.Декоратор cached_property выполняется только при обращении к атрибуту и только если атрибут с таким же именем не существует. При выполнении cached_property записывает значение в атрибут с тем же именем. Последующие чтения и записи атрибута имеют приоритет над методом cached_property, и он работает как обычный атрибут.
Кэшированное значение можно удалить, удалив атрибут. Это позволяет методу cached_property выполниться снова.
cached_property не предотвращает возможное состояние гонки при использовании в нескольких потоках. Функция-геттер может выполниться для одного экземпляра несколько раз, причём последним будет сохранено значение, полученное при последнем выполнении. Если кэшированное свойство идемпотентно или повторное выполнение для экземпляра не приводит к проблемам, это допустимо. Если нужна синхронизация, реализуйте необходимые блокировки внутри декорированной функции-геттера или вокруг обращения к кэшированному свойству.
Обратите внимание: этот декоратор влияет на работу словарей с совместным использованием ключей из PEP 412. Это означает, что словари экземпляров могут занимать больше места, чем обычно.
Кроме того, для этого декоратора требуется, чтобы атрибут
__dict__каждого экземпляра был изменяемым отображением. Поэтому он не будет работать с некоторыми типами, например с метаклассами (поскольку атрибуты__dict__экземпляров типов являются прокси пространства имён класса, доступными только для чтения), а также с типами, которые задают__slots__, не включая__dict__в число определённых слотов (такие классы вообще не предоставляют атрибут__dict__).Если изменяемое отображение недоступно или требуется экономно использовать совместное пространство ключей, аналогичного эффекта, что и у
cached_property(), можно добиться, разместивproperty()поверхlru_cache(). Подробнее о различиях сcached_property()см. в разделе Как кэшировать вызовы методов?.Добавлено в версии 3.8.
Изменено в версии 3.12: До Python 3.12 в
cached_propertyвходила недокументированная блокировка, которая гарантировала, что при использовании в нескольких потоках функция-геттер будет выполняться только один раз для каждого экземпляра. Однако блокировка была общей для каждого свойства, а не для каждого экземпляра, что могло приводить к неприемлемо высокой конкуренции за блокировку. В Python 3.12 и новее эта блокировка удалена.
-
functools.cmp_to_key(func) -
Преобразует функцию сравнения старого стиля в ключевую функцию. Используется с инструментами, которые принимают ключевые функции (например,
sorted(),min(),max(),heapq.nlargest(),heapq.nsmallest(),itertools.groupby()). Эта функция в первую очередь служит переходным инструментом для программ, переводимых с Python 2, где поддерживалось использование функций сравнения.Функция сравнения — это вызываемый объект, который принимает два аргумента, сравнивает их и возвращает отрицательное число, если первый аргумент меньше второго, ноль при равенстве или положительное число, если первый аргумент больше второго. Ключевая функция — это вызываемый объект, который принимает один аргумент и возвращает другое значение, используемое в качестве ключа сортировки.
Пример:
sorted(iterable, key=cmp_to_key(locale.strcoll)) # locale-aware sort order
Примеры сортировки и краткое руководство по сортировке см. в разделе Приёмы сортировки.
Добавлено в версии 3.2.
-
@functools.lru_cache(user_function) - @functools.lru_cache(maxsize=128, typed=False)
-
Декоратор, оборачивающий функцию в вызываемый объект с мемоизацией, который сохраняет до maxsize последних вызовов. Он позволяет сэкономить время, если дорогостоящая функция или функция, связанная с операциями ввода-вывода, периодически вызывается с одинаковыми аргументами.
Кэш потокобезопасен, поэтому обёрнутую функцию можно использовать в нескольких потоках. Это означает, что базовая структура данных сохранит целостность при одновременных обновлениях.
Обёрнутая функция может быть вызвана несколько раз, если другой поток выполнит дополнительный вызов до завершения и кэширования первоначального вызова.
Поскольку результаты кэшируются в словаре, позиционные и именованные аргументы функции должны быть хешируемыми.
Различные наборы аргументов могут считаться разными вызовами с отдельными записями в кэше. Например,
f(a=1, b=2)иf(b=2, a=1)различаются порядком именованных аргументов и могут иметь две отдельные записи в кэше.Если задан параметр user_function, он должен быть вызываемым объектом. Это позволяет применять декоратор lru_cache непосредственно к пользовательской функции, оставляя для maxsize значение по умолчанию 128:
@lru_cache def count_vowels(sentence): return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')Если для maxsize задано значение
None, функция LRU отключается, а размер кэша может расти без ограничений.Если для typed задано значение true, аргументы функции разных типов будут кэшироваться отдельно. Если typed имеет значение false, реализация обычно считает их эквивалентными вызовами и кэширует только один результат. (Некоторые типы, например str и int, могут кэшироваться отдельно даже при значении typed, равном false.)
Обратите внимание: учёт типов применяется только к непосредственным аргументам функции, а не к их содержимому. Скалярные аргументы
Decimal(42)иFraction(42)считаются разными вызовами с разными результатами. Напротив, кортежи-аргументы('answer', Decimal(42))и('answer', Fraction(42))считаются эквивалентными.К обёрнутой функции добавляется функция
cache_parameters(), которая возвращает новыйdictсо значениями параметров maxsize и typed. Она предназначена только для получения информации. Изменение значений не оказывает никакого эффекта.Чтобы оценить эффективность кэша и настроить параметр maxsize, к обёрнутой функции добавляется функция
cache_info(), возвращающая именованный кортеж со значениями hits, misses, maxsize и currsize.Декоратор также предоставляет функцию
cache_clear()для очистки или сброса кэша.Доступ к исходной базовой функции осуществляется через атрибут
__wrapped__. Он полезен для интроспекции, обхода кэша или повторного оборачивания функции с другим кэшем.Кэш хранит ссылки на аргументы и возвращаемые значения, пока они не устареют или пока кэш не будет очищен.
Если кэшируется метод, аргумент экземпляра
selfвключается в кэш. См. раздел Как кэшировать вызовы методов?Кэш LRU (вытеснение давно не использовавшихся элементов) лучше всего работает, когда последние вызовы позволяют наиболее точно предсказать будущие (например, самые популярные статьи на новостном сервере обычно меняются каждый день). Ограничение размера кэша гарантирует, что в долго работающих процессах, таких как веб-серверы, размер кэша не будет расти без ограничений.
В целом кэш LRU следует использовать только тогда, когда требуется повторно использовать ранее вычисленные значения. Поэтому нет смысла кэшировать функции с побочными эффектами, функции, которым при каждом вызове необходимо создавать отдельные изменяемые объекты (например, генераторы и асинхронные функции), или нечистые функции, такие как time() и random().
Пример кэша LRU для статического веб-содержимого:
@lru_cache(maxsize=32) def get_pep(num): 'Retrieve text of a Python Enhancement Proposal' resource = f'https://peps.python.org/pep-{num:04d}' try: with urllib.request.urlopen(resource) as s: return s.read() except urllib.error.HTTPError: return 'Not Found' >>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991: ... pep = get_pep(n) ... print(n, len(pep)) >>> get_pep.cache_info() CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)Пример эффективного вычисления чисел Фибоначчи с помощью кэша для реализации метода динамического программирования:
@lru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) >>> [fib(n) for n in range(16)] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] >>> fib.cache_info() CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)Добавлено в версии 3.2.
Изменено в версии 3.3: Добавлен параметр typed.
Изменено в версии 3.8: Добавлен параметр user_function.
Изменено в версии 3.9: Добавлена функция
cache_parameters()
-
@functools.total_ordering -
Для класса, определяющего один или несколько методов расширенного сравнения, этот декоратор класса предоставляет остальные методы. Это упрощает реализацию всех возможных операций расширенного сравнения:
Класс должен определить один из методов
__lt__(),__le__(),__gt__()или__ge__(). Кроме того, класс должен предоставлять метод__eq__().Например:
@total_ordering class Student: def _is_valid_operand(self, other): return (hasattr(other, "lastname") and hasattr(other, "firstname")) def __eq__(self, other): if not self._is_valid_operand(other): return NotImplemented return ((self.lastname.lower(), self.firstname.lower()) == (other.lastname.lower(), other.firstname.lower())) def __lt__(self, other): if not self._is_valid_operand(other): return NotImplemented return ((self.lastname.lower(), self.firstname.lower()) < (other.lastname.lower(), other.firstname.lower()))Примечание
Хотя этот декоратор упрощает создание корректно работающих полностью упорядоченных типов, за это приходится платить снижением производительности и усложнением трассировок стека для производных методов сравнения. Если результаты тестирования производительности показывают, что для данного приложения это является узким местом, самостоятельная реализация всех шести методов расширенного сравнения, вероятно, позволит легко повысить скорость.
Примечание
Этот декоратор не пытается переопределять методы, объявленные в самом классе или его суперклассах. Это означает, что если суперкласс определяет оператор сравнения, total_ordering не будет реализовывать его повторно, даже если исходный метод является абстрактным.
Добавлено в версии 3.2.
Изменено в версии 3.4: Теперь поддерживается возврат
NotImplementedиз базовой функции сравнения для нераспознанных типов.
-
functools.partial(func, /, *args, **keywords) -
Возвращает новый объект partial, который при вызове ведёт себя как функция func, вызванная с позиционными аргументами args и именованными аргументами keywords. Если при вызове передаются дополнительные аргументы, они добавляются к args. Если передаются дополнительные именованные аргументы, они дополняют и переопределяют keywords. Примерно эквивалентно:
def partial(func, /, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = {**keywords, **fkeywords} return func(*args, *fargs, **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfuncpartial()используется для частичного применения функции, «фиксируя» часть её аргументов и/или именованных аргументов и создавая новый объект с упрощённой сигнатурой. Например,partial()можно использовать для создания вызываемого объекта, который ведёт себя как функцияint(), где значение аргумента base по умолчанию равно двум:>>> from functools import partial >>> basetwo = partial(int, base=2) >>> basetwo.__doc__ = 'Convert base 2 string to an int.' >>> basetwo('10010') 18
-
class functools.partialmethod(func, /, *args, **keywords) -
Возвращает новый дескриптор
partialmethod, который ведёт себя какpartial, но предназначен для использования в определении метода, а не для непосредственного вызова.func должен быть дескриптором или вызываемым объектом (объекты, обладающие обоими свойствами, например обычные функции, обрабатываются как дескрипторы).
Если func является дескриптором (например, обычной функцией Python,
classmethod(),staticmethod(),abstractmethod()или другим экземпляромpartialmethod), вызовы__get__делегируются базовому дескриптору, а в результате возвращается соответствующий объект partial.Если func является вызываемым объектом, не являющимся дескриптором, динамически создаётся соответствующий связанный метод. При использовании в качестве метода он ведёт себя как обычная функция Python: аргумент self вставляется первым позиционным аргументом, даже перед аргументами args и keywords, переданными конструктору
partialmethod.Пример:
>>> class Cell: ... def __init__(self): ... self._alive = False ... @property ... def alive(self): ... return self._alive ... def set_state(self, state): ... self._alive = bool(state) ... set_alive = partialmethod(set_state, True) ... set_dead = partialmethod(set_state, False) ... >>> c = Cell() >>> c.alive False >>> c.set_alive() >>> c.alive True
Добавлено в версии 3.4.
-
functools.reduce(function, iterable, [initial, ]/) -
Применяет двухаргументную function к элементам iterable последовательно, слева направо, сводя итерируемый объект к одному значению. Например,
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])вычисляет((((1+2)+3)+4)+5). Левый аргумент, x, — это накопленное значение, а правый аргумент, y, — обновляемое значение из iterable. Если задан необязательный параметр initial, он помещается перед элементами итерируемого объекта при вычислении и служит значением по умолчанию, если итерируемый объект пуст. Если initial не задан, а iterable содержит только один элемент, возвращается первый элемент.Примерно эквивалентно:
initial_missing = object() def reduce(function, iterable, initial=initial_missing, /): it = iter(iterable) if initial is initial_missing: value = next(it) else: value = initial for element in it: value = function(value, element) return valueСм.
itertools.accumulate()— итератор, выдающий все промежуточные значения.
-
@functools.singledispatch -
Преобразует функцию в обобщённую функцию с диспетчеризацией по одному аргументу.
Чтобы определить обобщённую функцию, декорируйте её декоратором
@singledispatch. При определении функции с помощью@singledispatchобратите внимание, что диспетчеризация выполняется по типу первого аргумента:>>> from functools import singledispatch >>> @singledispatch ... def fun(arg, verbose=False): ... if verbose: ... print("Let me just say,", end=" ") ... print(arg)Чтобы добавить перегруженные реализации функции, используйте атрибут
register()обобщённой функции, который можно применять как декоратор. Для функций с аннотациями типов декоратор автоматически определяет тип первого аргумента:>>> @fun.register ... def _(arg: int, verbose=False): ... if verbose: ... print("Strength in numbers, eh?", end=" ") ... print(arg) ... >>> @fun.register ... def _(arg: list, verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem)Также можно использовать
types.UnionTypeиtyping.Union:>>> @fun.register ... def _(arg: int | float, verbose=False): ... if verbose: ... print("Strength in numbers, eh?", end=" ") ... print(arg) ... >>> from typing import Union >>> @fun.register ... def _(arg: Union[list, set], verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem) ...В коде, где не используются аннотации типов, соответствующий аргумент типа можно явно передать самому декоратору:
>>> @fun.register(complex) ... def _(arg, verbose=False): ... if verbose: ... print("Better than complicated.", end=" ") ... print(arg.real, arg.imag) ...В коде, где диспетчеризация выполняется по типу коллекции (например,
list), но требуется указать тип элементов коллекции (например,list[int]), тип диспетчеризации следует явно передать декоратору, а подсказку типа указать в определении функции:>>> @fun.register(list) ... def _(arg: list[int], verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem)Примечание
Во время выполнения функция будет выбирать реализацию по экземпляру списка независимо от типа его элементов; то есть
[1,2,3]будет обрабатываться так же, как["foo", "bar", "baz"]. Аннотация в этом примере предназначена только для средств статической проверки типов и не влияет на выполнение программы.Чтобы зарегистрировать лямбда-функции и уже существующие функции, атрибут
register()также можно использовать в функциональной форме:>>> def nothing(arg, verbose=False): ... print("Nothing.") ... >>> fun.register(type(None), nothing)Атрибут
register()возвращает исходную функцию без декоратора. Это позволяет комбинировать декораторы, использоватьpicklingи создавать модульные тесты для каждого варианта независимо:>>> @fun.register(float) ... @fun.register(Decimal) ... def fun_num(arg, verbose=False): ... if verbose: ... print("Half of your number:", end=" ") ... print(arg / 2) ... >>> fun_num is fun FalseПри вызове обобщённая функция выбирает реализацию по типу первого аргумента:
>>> fun("Hello, world.") Hello, world. >>> fun("test.", verbose=True) Let me just say, test. >>> fun(42, verbose=True) Strength in numbers, eh? 42 >>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True) Enumerate this: 0 spam 1 spam 2 eggs 3 spam >>> fun(None) Nothing. >>> fun(1.23) 0.615Если для конкретного типа не зарегистрирована реализация, для поиска более общей реализации используется порядок разрешения методов. Исходная функция, декорированная с помощью
@singledispatch, регистрируется для базового типаobject; это означает, что она используется, если не найдено более подходящей реализации.Если реализация зарегистрирована для абстрактного базового класса, виртуальные подклассы этого класса будут направляться к этой реализации:
>>> from collections.abc import Mapping >>> @fun.register ... def _(arg: Mapping, verbose=False): ... if verbose: ... print("Keys & Values") ... for key, value in arg.items(): ... print(key, "=>", value) ... >>> fun({"a": "b"}) a => bЧтобы проверить, какую реализацию обобщённая функция выберет для заданного типа, используйте атрибут
dispatch():>>> fun.dispatch(float) <function fun_num at 0x1035a2840> >>> fun.dispatch(dict) # note: default implementation <function fun at 0x103fe0000>
Чтобы получить доступ ко всем зарегистрированным реализациям, используйте доступный только для чтения атрибут
registry:>>> fun.registry.keys() dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>, <class 'decimal.Decimal'>, <class 'list'>, <class 'float'>]) >>> fun.registry[float] <function fun_num at 0x1035a2840> >>> fun.registry[object] <function fun at 0x103fe0000>Добавлено в версии 3.4.
Изменено в версии 3.7: Атрибут
register()теперь поддерживает использование аннотаций типов.Изменено в версии 3.11: Атрибут
register()теперь поддерживаетtypes.UnionTypeиtyping.Unionв качестве аннотаций типов.
-
class functools.singledispatchmethod(func) -
Преобразует метод в обобщённую функцию с диспетчеризацией по одному аргументу.
Чтобы определить обобщённый метод, декорируйте его декоратором
@singledispatchmethod. При определении функции с помощью@singledispatchmethodобратите внимание, что диспетчеризация выполняется по типу первого аргумента, отличного от self или cls:class Negator: @singledispatchmethod def neg(self, arg): raise NotImplementedError("Cannot negate a") @neg.register def _(self, arg: int): return -arg @neg.register def _(self, arg: bool): return not arg@singledispatchmethodподдерживает вложение других декораторов, таких как@classmethod. Обратите внимание: чтобы обеспечитьdispatcher.register,singledispatchmethodдолжен быть самым внешним декоратором. Ниже показан классNegator, методыnegкоторого связаны с классом, а не с его экземпляром:class Negator: @singledispatchmethod @classmethod def neg(cls, arg): raise NotImplementedError("Cannot negate a") @neg.register @classmethod def _(cls, arg: int): return -arg @neg.register @classmethod def _(cls, arg: bool): return not argЭтот же подход можно использовать с другими похожими декораторами:
@staticmethod,@~abc.abstractmethodи другими.Добавлено в версии 3.8.
-
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) -
Обновляет функцию-wrapper, чтобы она выглядела как обёрнутая функция wrapped. Необязательные аргументы — это кортежи, задающие, какие атрибуты исходной функции напрямую присваиваются соответствующим атрибутам функции-обёртки и какие атрибуты функции-обёртки обновляются соответствующими атрибутами исходной функции. Значениями по умолчанию для этих аргументов являются константы уровня модуля
WRAPPER_ASSIGNMENTS(присваивает функции-обёртке её__module__,__name__,__qualname__,__annotations__,__type_params__и__doc__— строку документации) иWRAPPER_UPDATES(обновляет__dict__функции-обёртки, то есть её словарь экземпляра).Чтобы обеспечить доступ к исходной функции для интроспекции и других целей (например, обхода декоратора кэширования, такого как
lru_cache()), эта функция автоматически добавляет функции-обёртке атрибут__wrapped__, ссылающийся на оборачиваемую функцию.Эта функция предназначена главным образом для функций декораторов, которые оборачивают декорируемую функцию и возвращают обёртку. Если функция-обёртка не обновлена, метаданные возвращённой функции будут отражать определение обёртки, а не исходной функции, что обычно мало полезно.
update_wrapper()можно использовать с вызываемыми объектами, отличными от функций. Любые атрибуты, указанные в assigned или updated, которые отсутствуют у оборачиваемого объекта, игнорируются (то есть эта функция не будет пытаться установить их для функции-обёртки). ИсключениеAttributeErrorпо-прежнему возникает, если у самой функции-обёртки отсутствуют какие-либо атрибуты, указанные в updated.Изменено в версии 3.2: Атрибут
__wrapped__теперь добавляется автоматически. Атрибут__annotations__теперь копируется по умолчанию. Отсутствие атрибутов больше не вызывает исключениеAttributeError.Изменено в версии 3.4: Атрибут
__wrapped__теперь всегда ссылается на обёрнутую функцию, даже если эта функция определила атрибут__wrapped__. (см. bpo-17482)Изменено в версии 3.12: Атрибут
__type_params__теперь копируется по умолчанию.
-
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) -
Это вспомогательная функция для вызова
update_wrapper()в качестве декоратора функции при определении обёрточной функции. Она эквивалентнаpartial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). Например:>>> from functools import wraps >>> def my_decorator(f): ... @wraps(f) ... def wrapper(*args, **kwds): ... print('Calling decorated function') ... return f(*args, **kwds) ... return wrapper ... >>> @my_decorator ... def example(): ... """Docstring""" ... print('Called example function') ... >>> example() Calling decorated function Called example function >>> example.__name__ 'example' >>> example.__doc__ 'Docstring'Без использования этой фабрики декораторов имя примерной функции было бы
'wrapper', а строка документации исходнойexample()была бы потеряна.
Объекты partial
Объекты partial — это вызываемые объекты, создаваемые с помощью partial(). У них есть три атрибута только для чтения:
-
partial.func -
Вызываемый объект или функция. Вызовы объекта
partialбудут передаваться вfuncс новыми аргументами и именованными аргументами.
-
partial.args -
Крайние левые позиционные аргументы, которые будут добавлены перед позиционными аргументами, переданными при вызове объекта
partial.
-
partial.keywords -
Именованные аргументы, которые будут переданы при вызове объекта
partial.
Объекты partial подобны объектам функций: они вызываемы, поддерживают слабые ссылки и могут иметь атрибуты. Однако есть несколько важных различий. Например, атрибуты __name__ и function.__doc__ не создаются автоматически. Кроме того, объекты partial, определённые в классах, ведут себя как статические методы и не преобразуются в связанные методы при поиске атрибута экземпляра.
© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/library/functools.html