Нові круті фічі в Python 3.7

5 хв. читання

Python 3.7 знаходився в розробці з вересня 2016. В кінці червня ця версія була офіційно випущена.

Що ж вона собою привнесла? Хоча документація надає хороший огляд нових функцій, ця стаття поглибиться в деякі з аспектів релізу. Серед яких:

  • Легший доступ до налагоджувачів через вбудовану функцію breakpoint().
  • Просте створення класів, використовуючи класи даних.
  • Налаштовуваний доступ до атрибутів модулів.
  • Покращена підтримка type hinting.
  • Більш висока точність функцій синхронізації за часом.

Що ще більш важливо, Python 3.7 швидкий.

У кінцевих розділах цієї статті ви більше дізнаєтесь про цю швидкість, так само як і про інші круті функції Python 3.7. Також ви отримаєте поради щодо оновлення до нової версії.

Вбудована функція breakpoint()

Хоча ми повинні прагнути писати ідеальний код, істина полягає в тому, що ми ніколи не зможемо цього зробити. Налагодження є важливою частиною програмування. Python 3.7 вводить нову вбудовану функцію — breakpoint(). Насправді, це не додає ніякої нової функціональності у Python, але робить використання налагоджувачів більш гнучким та інтуїтивним.

Припустимо, що у вас є наступний забагований код у файлі bugs.py:

def divide(e, f):
    return f / e

a, b = 0, 1
print(divide(a, b))

Запуск коду спричинить помилку ZeroDivisionError всередині divide(). Скажімо, ви хочете перервати код та перейти до налагоджувача безпосередньо у верхній частині divide(). Ви можете це зробити, встановивши так звану «точку зупину» у вашому коді:

def divide(e, f):
    # Вставте точку зупину тут
    return f / e

Точка зупину — сигнал всередині вашого коду, виконання якого повинно тимчасово зупинитися, для того, щоб ви могли подивитися на поточний стан програми. Як розмістити точку зупину? У Python 3.6 й нижче ви використовуєте цей загадковий рядок:

def divide(e, f):
    import pdb; pdb.set_trace()
    return f / e

Тут pdb — налагоджувач зі стандартної бібліотеки. Натомість, у Python 3.7 ви можете використовувати виклик нової функції breakpoint() як скорочений шлях:

def divide(e, f):
    breakpoint()
    return f / e

breakpoint() спочатку імпортує pdb, а потім викликає pdb.set_trace(). Основними перевагами є те, що breakpoint() легше запам'ятати, і те, що вам всього лише треба ввести 12 символів замість 27. Однак, реальним бонусом використання breakpoint() є його налаштовуваність.

Запустіть ваш скрипт bugs.py з breakpoint():

$ python3.7 bugs.py 
> /home/gahjelle/bugs.py(3)divide()
-> return f / e
(Pdb)

Скрипт перерветься, коли досягне breakpoint() й перенесе вас у сеанс налагодження PDB. Ви можете ввести c й натиснути Enter, щоб продовжити виконання скрипта. Зверніться до цього посібника по PDB, якщо хочете більше дізнатися про PDB та налагодження.

Тепер скажімо, ви вважаєте, що ви виправили баг. Ви захочете знову запустити скрипт, але не зупиняючись в налагоджувачі. Авжеж, ви можете закоментувати рядок breakpoint(), але є інший варіант — використати змінну середовища PYTHONBREAKPOINT. Ця змінна контролює поведінку breakpoint(), а встановлення PYTHONBREAKPOINT=0 означає, що будь-який виклик breakpoint() буде ігноруватися:

$ PYTHONBREAKPOINT=0 python3.7 bugs.py
ZeroDivisionError: division by zero

Упс, здається, що ви ще не виправили помилку...

Ще один варіант — використати PYTHONBREAKPOINT для визначення іншого налагоджувача, а не PDB. Наприклад, для використання PuDB (візуальний налагоджувач у консолі) ви можете прописати:

$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py

Для того, щоб все працювало, вам потрібно мати встановлений pudb (pip install pudb). А Python потурбується про те, щоб імпортувати pudb. Таким чином ви також можете встановити налагоджувач за замовчуванням. Просто задайте змінній середовища значення, яке відповідає налагоджувачу, якому ви надаєте перевагу. Подивіться в цьому посібнику інструкції зі встановлення змінної середовища у вашій системі.

Нова функція breakpoint() працює не тільки з налагоджувачами. Одним зі зручних варіантів її використання може бути просто запуск інтерактивної оболонки у вашому коді. Наприклад. для запуску сеансу IPython, можна використати наступне:

$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py 
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print(e / f)
0.0

Ви також можете створити свою власну функцію й викликати її через breakpoint(). Наступний код виводить всі змінні в локальній області видимості. Додайте його у файл bp_utils.py:

from pprint import pprint
import sys

def print_locals():
    caller = sys._getframe(1)  # Caller is 1 frame up.
    pprint(caller.f_locals)

Щоб використати цю функцію, як і в прикладах вище, потрібно встановити значення для PYTHONBREAKPOINT за допомогою нотації <module>.<function>:

$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py 
{'e': 0, 'f': 1}
ZeroDivisionError: division by zero

Як правило, breakpoint() буде використовуватись для виклику функцій та методів, що не потребують аргументів. Однак, також можливо й передавати аргументи. Змініть breakpoint() в bugs.py на:

breakpoint(e, f, end="<-END\
")

Примітка: налагоджувач за замовчуванням PDB викличе помилку TypeError у цьому рядку, оскільки pdb.set_trace() не приймає ніяких позиційних аргументів


Запустіть цей код. в якому breakpoint() маскується під функцію print(), щоб побачити простий приклад того, як передаються аргументи:

$ PYTHONBREAKPOINT=print python3.7 bugs.py 
0 1<-END
ZeroDivisionError: division by zero

Перегляньте PEP 553, так само як і документацію для breakpoint() та sys.breakpointhook() для отримання додаткової інформації.

Класи даних

Новий модуль dataclasses дозволяє зручніше писати власні класи, оскільки спеціальні методи, такі як .__init__(), .__repr__() та .__eq__(), додаються автоматично. Використовуючи декоратор @dataclass, ви можете писати написати щось на зразок цього:

from dataclasses import dataclass, field

@dataclass(order=True)
class Country:
    name: str
    population: int
    area: float = field(repr=False, compare=False)
    coastline: float = 0

    def beach_per_person(self):
        """Метри берегової лінії на людину"""
        return (self.coastline * 1000) / self.population

Ці дев'ять рядків коду стоять за доволі великою кількістю шаблонного коду й найкращих практик. Подумайте про те, скільки всього знадобиться для реалізації Country як регулярного класу: метод .__init__(), repr, шість різних методів порівняння, а також метод .beach_per_person(). Так виглядала б реалізація цього класу, яка приблизно еквівалентна класу даних:

class Country:

    def __init__(self, name, population, area, coastline=0):
        self.name = name
        self.population = population
        self.area = area
        self.coastline = coastline

    def __repr__(self):
        return (
            f"Country(name={self.name!r}, population={self.population!r},"
            f" coastline={self.coastline!r})"
        )

    def __eq__(self, other):
        if other.__class__ is self.__class__:
            return (
                (self.name, self.population, self.coastline)
                == (other.name, other.population, other.coastline)
            )
        return NotImplemented

    def __ne__(self, other):
        if other.__class__ is self.__class__:
            return (
                (self.name, self.population, self.coastline)
                != (other.name, other.population, other.coastline)
            )
        return NotImplemented

    def __lt__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) < (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __le__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) <= (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __gt__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) > (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __ge__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) >= (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def beach_per_person(self):
        """Meters of coastline per person"""
        return (self.coastline * 1000) / self.population

Після створення клас даних стає звичайним класом. Наприклад, ви можете наслідувати з нього стандартним способом. Основна ціль класів даних — прискорити й спростити написання надійних класів, зокрема невеликих, які в основному зберігають дані.

Ви можете використовувати клас даних Country як будь-який інший клас:

>>> norway = Country("Norway", 5320045, 323802, 58133)
>>> norway
Country(name='Norway', population=5320045, coastline=58133)

>>> norway.area
323802

>>> usa = Country("United States", 326625791, 9833517, 19924)
>>> nepal = Country("Nepal", 29384297, 147181)
>>> nepal
Country(name='Nepal', population=29384297, coastline=0)

>>> usa.beach_per_person()
0.06099946957342386

>>> norway.beach_per_person()
10.927163210085629

Зверніть увагу на те, що при ініціалізації класу використовуються всі поля .name, .population, .area, та .coastline (хоча .coastline є опціональним, як показано на прикладі Непалу, який не має виходу до моря). Клас Country має прийнятний repr, в той час, як визначення методів працює так само, як і для звичайних класів.

За замовчуванням класи даних можна порівнювати для рівності. Оскільки ми вказали order=True у декораторі @dataclass, клас Country також можна сортувати:

>>> norway == norway
True

>>> nepal == usa
False

>>> sorted((norway, usa, nepal))
[Country(name='Nepal', population=29384297, coastline=0),
 Country(name='Norway', population=5320045, coastline=58133),
 Country(name='United States', population=326625791, coastline=19924)]

Сортування відбувається за значеннями полів, спочатку .name, потім .population й так далі. Однак, якщо ви використовуєте field(), ви можете налаштувати, які поля будуть використовуватись у порівнянні. У цьому прикладі поле .area було виключено з repr та порівнянь.

Класи даних виконують деякі з тих же дій, що й namedtuple. Однак, найбільше всього вони надихаються проектом attrs. Перегляньте цей посібник по класах даних для більшої кількості прикладів та додаткової інформації, а також PEP 557 для офіційного опису.

Кастомізація атрибутів модулів

Атрибути в Python повсюди! Хоча атрибути класів є, вірогідно, найбільш відомими, атрибути можуть бути додані практично у все — включаючи функції та модулі. Деякі з базових функцій Python реалізовані як атрибути: більшість функціональності самоаналізу, рядків документації та просторів імен. Функції всередині модуля стають доступними як атрибути модуля.

Найчастіше атрибути вилучаються за допомогою крапкової нотації: thing.attribute. Однак, ви також можете отримати атрибути, які іменуються під час виконання, використовуючи getattr():

import random

random_attr = random.choice(("gammavariate", "lognormvariate", "normalvariate"))
random_func = getattr(random, random_attr)

print(f"A {random_attr} random value: {random_func(1, 1)}")

Запуск цього коду призведе до створення чогось на кшталт цього:

A gammavariate random value: 2.8017715125270618

Для класів виклик thing.attr спочатку шукатиме attr, визначений для thing. Якщо він не знайдений, тоді викликається спеціальний метод thing.__getattr__("attr"). (це спрощення. Більш докладну інформацію дивіться в цій статті). Метод .__getattr__() може бути використаний для налаштування доступу до атрибутів об'єктів.

До Python 3.7 таке ж саме налаштування було нелегким для атрибутів модулів. Однак, PEP 562 вводить у модулі __getattr__(), разом з відповідною функцією __dir__(). Спеціальна функція __dir__() дозволяє налаштувати результат виклику dir() у модулі.

Сам по собі PEP надає кілька прикладів використання цих функцій, включаючи додавання попередження про небажану до використання функціональність (deprecation warnings) у функції та lazy завантаження важких підмодулів. Нижче ми створимо просту систему плагінів, яка дозволить динамічно додавати функції у модуль. Цей приклад використовує пакети Python. Зверніться до цієї статті, якщо вам треба освіжити знання про пакети.

Створіть нову теку plugins та додайте наступний код у файл plugins/__init__.py:

from importlib import import_module
from importlib import resources

PLUGINS = dict()

def register_plugin(func):
    """Декоратор для регістрації плагінів"""
    name = func.__name__
    PLUGINS[name] = func
    return func

def __getattr__(name):
    """Повернення іменованого плагіну"""
    try:
        return PLUGINS[name]
    except KeyError:
        _import_plugins()
        if name in PLUGINS:
            return PLUGINS[name]
        else:
            raise AttributeError(
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

def __dir__():
    """Список доступних плагінів"""
    _import_plugins()
    return list(PLUGINS.keys())

def _import_plugins():
    """Імпорт всіх ресурсів для реєстрації плагінів"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

Перед тим, як ми подивимось, що робить цей код, додайте ще два файли у теку plugins. Подивімося спочатку на файл plugins/plugin_1.py:

from . import register_plugin

@register_plugin
def hello_1():
    print("Hello from Plugin 1")

Потім додамо аналогічний код у файл plugins/plugin_2.py:

from . import register_plugin

@register_plugin
def hello_2():
    print("Hello from Plugin 2")

@register_plugin
def goodbye():
    print("Plugin 2 says goodbye")

Ці плагіни тепер можуть бути використані наступним чином:

>>> import plugins
>>> plugins.hello_1()
Hello from Plugin 1

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.goodbye()
Plugin 2 says goodbye

Можливо, все це не здається таким революційним (і, вірогідно, воно таким і не є), але подивіться на те, що тут насправді сталося. Зазвичай, щоб мати можливість викликати plugins.hello_1(), функція hello_1() має бути визначена у модулі plugins або явно імпортована в __init__.py у пакеті plugins. А тут це не так!

Замість цього, hello_1() визначається у довільному файлі всередині пакету plugins. hello_1() стає частиною пакету plugins, зареєструвавши себе за допомогою декоратора @register_plugin.

Різниця ледь помітна. Замість пакета, який визначає, які функції будуть доступні, окремі функції реєструються як частина пакету. Це дає вам просту структуру, де ви можете додавати функції незалежно від решти коду, не зберігаючи централізований список доступних функцій.

Зробімо короткий огляд того, що __getattr__() робить всередині коду plugins/__init__.py. Коли ви запитуєте plugins.hello_1(), Python спочатку шукатиме функцію hello_1() всередині файлу plugins/__init__.py. Через те, що така функція не існує, замість цього Python викличе __getattr__("hello_1"). Пам'ятаєте вихідний код функції __getattr__():

def __getattr__(name):
    """Повертає іменований плагін"""
    try:
        return PLUGINS[name]        # 1) Спроба повернути плагін
    except KeyError:
        _import_plugins()           # 2) Імпорт всіх плагінів
        if name in PLUGINS:
            return PLUGINS[name]    # 3) Повторна спроба повернути плагін
        else:
            raise AttributeError(   # 4) Генерування помилки
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

__getattr__() містить наступні кроки. Номера в списку відповідають пронумерованим коментарям в коді:

  1. Спочатку функція оптимістично намагається повернути іменований плагін зі словника PLUGINS. Це спроба буде успішною, якщо плагін з ім'ям name існує й вже був імпортований.
  2. Якщо іменований плагін не був знайдений у словнику PLUGINS, ми впевнимось, що всі плагіни були імпортовані.
  3. Повертаємо іменований плагін, якщо він став доступний після імпорту.
  4. Якщо після імпортування плагінів, шуканого плагіну досі немає в словнику PLUGINS, ми генеруємо помилку AttributeError, повідомляючи, що name не є атрибутом (плагіном) поточного модуля.

Як же заповнюється словник PLUGINS? Функція _import_plugins() імпортує всі файли Python всередину пакету plugins, але, здається, не торкається PLUGINS:

def _import_plugins():
    """Імпорт всіх ресурсів для реєстрації плагінів"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

Не забувайте, що до кожної функції плагіну залучений декоратор @register_plugin. Цей декоратор викликається коли плагіни імпортуються й фактично заповнюють словник PLUGINS. Ви можете це побачити, якщо вручну імпортуєте один з файлів плагінів:

>>> import plugins
>>> plugins.PLUGINS
{}

>>> import plugins.plugin_1
>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>}

Продовжуючи приклад, що виклик dir() на модулі також імпортує плагіни, що залишилися:

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>,
 'hello_2': <function hello_2 at 0x7f29d4341620>,
 'goodbye': <function goodbye at 0x7f29d43416a8>}

dir() зазвичай перераховує всі доступні атрибути для об'єкта. Як правило, використання dir() в модулі призводить до чогось на зразок цього:

>>> import plugins
>>> dir(plugins)
['PLUGINS', '__builtins__', '__cached__', '__doc__',
 '__file__', '__getattr__', '__loader__', '__name__',
 '__package__', '__path__', '__spec__', '_import_plugins',
 'import_module', 'register_plugin', 'resources']

Тоді як це може бути корисною інформацією, ми більш зацікавлені в демонстрації доступних плагінів. У Python 3.7 можна налаштувати виклик dir() в модулі додавши спеціальну функцію __dir__(). Для plugins/__init__.py ця функція спочатку переконується, що все плагіни були імпортовані, а потім вже перераховує їх імена:

def __dir__():
    """Список доступних плагінів"""
    _import_plugins()
    return list(PLUGINS.keys())

Перш, ніж залишити цей приклад, зверніть увагу, що ми також використали іншу нову функцію Python 3.7. Для імпортування всіх модулів в теку plugins ми скористались модулем importlib.resources. Цей модуль надає доступ до файлів та ресурсів всередині модулів та пакетів без потреби використання __file__ (який не завжди працює) або pkg_resources (який повільний).

Покращення типізації

Рекомендація типів та анотації постійно розвивалися на протязі релізів нових версій Python 3. Рекомендація типів Python тепер доволі стабільна. Тим не менш, Python 3.7 привносить деякі покращення: покращена продуктивність, підтримка ядер та прямі посилання.

Python не робить перевірку типів під час виконання (тільки якщо ви не використаєте явно пакети на зразок enforce). Тому додавання рекомендації типів до вашого коду не повинно впливати на його продуктивність.

На жаль, це не зовсім правда, оскільки більшість рекомендацій типів потребують модуля typing. Цей модуль один з найповільніших модулів в стандартній бібліотеці. PEP 560 додає підтримку типізації в Python 3.7, що значно прискорює роботу модуля typing.

Хоча система типів Python достатньо виразна, є одна проблема, яка спричиняє біль — прямі посилання. Рекомендації типів — або, в більш загальному плані, анотації — обчислюються під час імпорту модуля. Тому всі імена мають бути визначені перед тим, як будуть використані. Наступний код неможливо виконати:

class Tree:
    def __init__(self, left: Tree, right: Tree) -> None:
        self.left = left
        self.right = right

Запуск коду призведе до NameError, тому що клас Tree ще не визначений (повністю) у визначенні метода .__init__():

Traceback (most recent call last):
  File "tree.py", line 1, in <module>
    class Tree:
  File "tree.py", line 2, in Tree
    def __init__(self, left: Tree, right: Tree) -> None:
NameError: name 'Tree' is not defined

Для подолання цього вам знадобилось би написати Tree у вигляді рядкового літерала:

class Tree:
    def __init__(self, left: "Tree", right: "Tree") -> None:
        self.left = left
        self.right = right

Подивіться PEP 484 для початкового обговорення.

У майбутньому Python 4.0 так звані прямі посилання будуть дозволені. Він не буде обробляти анотації, поки це не буде явно вказано. PEP 563 описує деталі цієї пропозиції. У Python 3.7 прямі посилання вже доступні як __future__ import. Тепер ви можете писати наступне:

from __future__ import annotations

class Tree:
    def __init__(self, left: Tree, right: Tree) -> None:
        self.left = left
        self.right = right

Зверніть увагу на те, що окрім уникнення дещо незграбного синтаксису Tree, відкладене обчислення анотацій також прискорить ваш код, оскільки рекомендації типів не виконуються. Прямі посилання вже підтримуються за допомогою mypy.

Безумовно, найбільш поширеним використанням анотацій є рекомендації типів. Тим не менш, ви маєте повний доступ до анотацій під час виконання й можете використовувати їх на свій розсуд. Якщо ви обробляєте анотації напряму, вам необхідно взаємодіяти з можливими прямими посиланнями явно.

Створімо кілька безглуздих прикладів, які показують, коли анотації обчислюються. Спершу ми зробимо це в старому стилі, тому анотації обчислюватимуться під час імпорту. Нехай anno.py міститиме наступний код:

def greet(name: print("Now!")):
    print(f"Hello {name}")

Зверніть увагу на те, що анотація nameprint(). Це тільки для того, щоб точно побачити, коли анотація обчислюється. Імпортуйте новий модуль:

>>> import anno
Now!

>>> anno.greet.__annotations__
{'name': None}

>>> anno.greet("Alice")
Hello Alice

Як ви можете побачити, анотація обчислюється під час імпорту. Відмітьте, що name закінчує анотуватися з None, тому що це значення print(), що повертається.

Додайте __future__ import, щоб дозволити відкладене обчислення анотацій.

from __future__ import annotations

def greet(name: print("Now!")):
    print(f"Hello {name}")

Імпорт цього оновленого коду не буде обчислювати анотацію:

>>> import anno

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

>>> anno.greet("Marty")
Hello Marty

Зверніть увагу на те, що Now! ніколи не друкується й анотація в словнику __annotations__ зберігається як рядковий літерал. Щоб обчислити анотацію використайте typing.get_type_hints() або eval():

>>> import typing
>>> typing.get_type_hints(anno.greet)
Now!
{'name': <class 'NoneType'>}

>>> eval(anno.greet.__annotations__["name"])
Now!

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

Зауважте також, що словник __annotations__ ніколи не оновлюється, тому вам не потрібно обчислювати анотацію кожного разу, коли ви його використовуєте.

Точність синхронизації

В Python 3.7 модуль time отримує деякі нові функції, як описано в PEP 564. Зокрема, додаються шість наступних функцій:

  • clock_gettime_ns(): повертає час вказаного годинника;
  • clock_settime_ns(): встановлює час заданого годинника;
  • monotonic_ns(): повертає час відносного годинника, який не може повернутися назад (наприклад, через перехід на літній час);
  • perf_counter_ns():повертає значення лічильника продуктивності — годинник, спеціально призначений для виміру коротких інтервалів;
  • process_time_ns(): повертає суму системного й користувацького часу центрального процесора поточного процесу (не враховуючи час сну);
  • time_ns(): повертає кількість наносекунд з 1-го січня 1970.

У певному сенсі, нова функціональність не додана. Кожна функція ідентична до вже наявних функцій без суфікса _ns. Різниця в тому, що нові функції повертають кількість наносекунд як int, а не як float.

Для більшості застосунків різниця між цими новими наносекундними функціями та їх старими аналогами не буде помітною. Однак, нові функції легше обґрунтувати, оскільки вони покладаються на int, а не на float. Числа з рухомою комою за своєю природою неточні:

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Це не проблема Python, але скоріше проблема комп'ютерів, які мають представляти нескінченні десяткові числа з використанням кінцевого числа бітів.

float в Python слідує стандарту IEEE 754 й використовує 53 значущих біти. Результат полягає в тому, що будь-який час, що перевищує 104 дні (2⁵³ або приблизно 9 квадрильйонів секунд) не може бути виражений float з точністю до наносекунд. На відміну від цього, int в Python необмежений, тому ціле число наносекунд завжди матиме наносекундну точність незалежно від значення часу.

Наприклад, time.time() повертає кількість секунд з 1-го січня 1970. Це число вже досить велике, тому його точність знаходиться на рівні мікросекунд. Ця функція є одною з тих, яка демонструє найбільше покращення у версії _ns. Розподільна здатність time.time_ns() у три рази краща, ніж для time.time().

Крім того, якщо вам потрібно працювати с датами й часом з точністю до наносекунд, стандартна бібліотека datetime не буде її зрізати. Вона обробляє явно тільки мікросекунди:

>>> from datetime import datetime, timedelta
>>> datetime(2018, 6, 27) + timedelta(seconds=1e-6)
datetime.datetime(2018, 6, 27, 0, 0, 0, 1)

>>> datetime(2018, 6, 27) + timedelta(seconds=1e-9)
datetime.datetime(2018, 6, 27, 0, 0)

Натомість, ви можете скористатись проектом astropy. Його пакет astropy.time представляє дату й час через два об'єкти float, що гарантує «субнаносекундну точність в часі, що охоплює вік Всесвіту».

>>> from astropy.time import Time, TimeDelta
>>> Time("2018-06-27")
<Time object: scale='utc' format='iso' value=2018-06-27 00:00:00.000>

>>> t = Time("2018-06-27") + TimeDelta(1e-9, format="sec")
>>> (t - Time("2018-06-27")).sec
9.976020010071807e-10

Остання версія astropy доступна в Python 3.5 й пізніших версіях.

Інші досить круті нововведення

Досі ви бачили заголовки новин відносно того, що нового в Python 3.7. Однак, є доволі багато інших змін, які також доволі круті. У цьому розділі ми коротко розглянемо деякі з них.

Гарантований порядок словників

CPython реалізація Python 3.6 має впорядковані словники (PyPy також). Це означає, що елементи в словниках ітеруються в тому ж самому порядку, в якому вони були вставлені. В першому прикладі використовується Python 3.5, а в другому — Python 3.6:

>>> {"one": 1, "two": 2, "three": 3}  # Python <= 3.5
{'three': 3, 'one': 1, 'two': 2}

>>> {"one": 1, "two": 2, "three": 3}  # Python >= 3.6
{'one': 1, 'two': 2, 'three': 3}

У Python 3.6 цей порядок був просто хорошим наслідком реалізації dict. Однак, в Python 3.7 словники. які зберігають свій порядок вставки, є частиною специфікації мови. Таким чином, тепер на нього можна покладатися в проектах, які підтримують тільки Python >= 3.7 (або CPython >= 3.6).

async та await — ключові слова

Python 3.5 ввів корутини (goroutines) з синтаксисом async та await. Щоб уникнути проблем зворотної сумісності, async та await не були додані в список зарезервованих ключових слів. Іншими словами, все ще було можливо визначати змінні або функції з ім'ям async або await.

В Python 3.7 це вже неможливо:

>>> async = 1
  File "<stdin>", line 1
    async = 1
          ^
SyntaxError: invalid syntax

>>> def await():
  File "<stdin>", line 1
    def await():
            ^
SyntaxError: invalid syntax

Модернізація asyncio

Контекстні змінні — змінні, які можуть мати різні значення в залежності від їх контексту. Вони подібні до Thread-Local Storage, в якому кожен потік виконання може мати інше значення для змінної. Однак, у контекстних змінних може бути кілька контекстів в одному потоці виконання. Головним варіантом використання контекстних змінних є відстеження змінних в паралельних асинхронних задачах.

В наступному прикладі створені три контексти, кожний зі своїм значенням для значення name. Функція greet() пізніше зможе використовувати значення name всередині кожного контексту:

import contextvars

name = contextvars.ContextVar("name")
contexts = list()

def greet():
    print(f"Hello {name.get()}")

# Створення контекстів та встановлення назви контекстної змінної
for first_name in ["Steve", "Dina", "Harry"]:
    ctx = contextvars.copy_context()
    ctx.run(name.set, first_name)
    contexts.append(ctx)

# Запуск функції greet всередині кожного контексту
for ctx in reversed(contexts):
    ctx.run(greet)

Запущений скрипт вітає Стіва, Діна й Гаррі у зворотному порядку:

$ python3.7 context_demo.py
Hello Harry
Hello Dina
Hello Steve

Імпорт файлів даних за допомогою importlib.resources

Одна з проблем при пакуванні в Python проекті — вирішити, що робити з проектними ресурсами, такими як файли даних, необхідні для проекту. Зазвичай використовуються кілька варіантів:

  • Хардкодинг шляху до файлу даних.
  • Поміщення файлу даних всередину пакету та його локалізація. використовуючи __file__.
  • Використання setuptools.pkg_resources для доступу до ресурсу файлу даних.

У кожного з них є свої недоліки. Перший варіант не переносний. Використання __file__ більш портативне, але якщо Python проект встановлений, він може опинитися всередині zip і не мати атрибуту __file__. Третій варіант вирішує цю проблему, але, на жаль, він дуже повільний.

Кращим рішенням є новий модуль importlib.resources в стандартній бібліотеці. Він використовує наявну в Python функціональність імпорту, щоб також імпортувати файли даних. Припустимо, що у вас є ресурс всередині пакету Python на зразок цього:

data/
│
├── alice_in_wonderland.txt
└── __init__.py

Зауважте, що data має бути пакетом Python. Тобто, тека має містити файл __init__.py (який може бути пустим). Потім ви зможете прочитати файл alice_in_wonderland.txt наступним чином:

>>> from importlib import resources
>>> with resources.open_text("data", "alice_in_wonderland.txt") as fid:
...     alice = fid.readlines()
... 
>>> print("".join(alice[:7]))
CHAPTER I. Down the Rabbit-Hole

Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, 'and what is the use of a book,' thought Alice 'without pictures or
conversations?'

Аналогічна функція resources.open_binary() доступна для відкриття файлів у двійковому режимі. У минулому прикладі «плагіни як атрибути модуля» ми використовували importlib.resources для виявлення доступних плагінів, використовуючи resources.contents(). Більш докладну інформацію дивіться в презентації Barry Warsaw's PyCon 2018 talk.

Можна використовувати importlib.resources в Python 2.7 та Python 3.4+ через backport. Доступний посібник із переходу з pkg_resources в importlib.resources.

Трюки розробника

Python 3.7 додав декілька функцій, націлених на вас, як на розробника. Ви вже бачили нову вбудовану функцію breakpoint(). На додачу, в інтерпретатор Python було додано кілька нових параметрів командного рядка -X.

Ви можете легко зрозуміти, скільки часу займають імпорти у вашому скрипті, використавши -X importtime:

$ python3.7 -X importtime my_script.py
import time: self [us] | cumulative | imported package
import time:      2607 |       2607 | _frozen_importlib_external
...
import time:       844 |      28866 |   importlib.resources
import time:       404 |      30434 | plugins

Стовпець cumulative показує кумулятивний час імпорту (в мікросекундах). У цьому прикладі імпортування plugins займає приблизно 0.03 секунди, більшість з яких було витрачено на імпорт importlib.resources. Стовпець self показує час імпорту, за винятком вкладених імпортів.

Тепер ви можете прописати -X dev для активації «режиму розробки». Режим розробки додасть деякі функції налагодження й перевірки часу виконання, які вважаються занадто повільними, щоб бути ввімкненими за замовчуванням. Вони включають можливість faulthandler показувати трасування після серйозних збоїв, а також більше попереджень та налагоджувальних хуків.

Нарешті, -X utf8 включає режим UTF-8 (див. PEP 540). У цьому режимі UTF-8 буде використовуватися для кодування тексту, незалежно від поточної локалі.

Оптимізації

Кожна нова версія Python поставляється з набором оптимізацій. У Python 3.7 є деякі істотні прискорення, в тому числі:

  • Менше накладних витрат при виклику.
  • Виклики методів в цілому на 20% швидші.
  • Час запуску самого Python зменшився на 10-30%.
  • Імпорт typing у 7 разів швидше.

Крім того, включено багато інших, більш специфічних оптимізацій. Перегляньте цей список для більш детального огляду.

Результатом всіх цих оптимізацій є те, що Python 3.7 працює швидко. Це просто найшвидша версія CPython зі всіх випущених.

Отже, чи варто оновлюватись?

Почнімо з простої відповіді. Якщо ви хочете спробувати нові функції, які ви бачили тут, тоді вам потрібно використати Python 3.7. Використання таких інструментів, як pyenv або Anaconda дозволяє легко встановити декілька версій Python поруч. Немає ніяких недоліків в тому, щоб встановити Python 3.7 й спробувати його в дії.

Тепер перейдемо до більш складних питань. Чи слід вам оновлювати ваше робоче середовище до Python 3.7? Чи слід вам робити власний проект, залежний від Python 3.7, щоб скористатися перевагами нових функцій?

З очевидним попередженням про те, що ви завжди маєте проводити ретельне тестування перед оновленням робочого середовища, в Python 3.7 дуже мало речей, які можуть поламати ваш більш ранній код (async та await, які стали ключовими словами — один з прикладів). Якщо ви вже використовуєте сучасну версію Python, оновлення до 3.7 має пройти досить гладко.

Стверджувати те, що ви маєте зробити свій проект в 3.7, складніше. Багато нових функцій Python 3.7 або доступні як backports для Python 3.6 (класи даних, importlib.resources) або зручності (швидший запуск застосунків та виклик методів, легше налагодження та параметри -X). І останнє — ви можете скористатись перевагами Python 3.7, зберігаючи код сумісним з Python 3.6 (або нижче).

Серед великих особливостей, які замкнуть ваш код тільки на версії Python 3.7 — __getattr__() в модулях, прямі посилання в рекомендаціях типів й наносекундні часові функції. Якщо вам дійсно потрібно щось з цього, тоді вам слід догодити своїм вимогам. Інакше, ваш проект буде більш корисним для інших, якщо він зможе працювати на Python 3.6 на деякий час довше.

Перегляньте цей посібник з портування на Python 3.7 для деталей, які слід знати при оновленні.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація