company_banner

Подборка @pythonetc, август 2019



    Новая подборка советов про Python и программирование из моего авторского канала @pythonetc.

    Предыдущие подборки


    Если у экземпляра класса нет атрибута с заданным именем, то он пытается обратиться к атрибуту класса с тем же именем.

    >>> class A:
    ...     x = 2
    ...
    >>> A.x
    2
    >>> A().x
    2

    Экземпляр легко может иметь атрибут, которого нет у класса, или иметь атрибут с другим значением:

    >>> class A:
    ...     x = 2
    ...     def __init__(self):
    ...         self.x = 3
    ...         self.y = 4
    ...
    >>> A().x
    3
    >>> A.x
    2
    >>> A().y
    4
    >>> A.y
    AttributeError: type object 'A' has no attribute 'y'

    Если же вы хотите, чтобы экземпляр вёл себя так, словно у него нет атрибута, хотя он есть у класса, то придётся создать кастомный дескриптор, который запрещает обращаться из этого экземпляра:

    class ClassOnlyDescriptor:
        def __init__(self, value):
            self._value = value
            self._name = None  # see __set_name__
    
        def __get__(self, instance, owner):
            if instance is not None:
                raise AttributeError(
                    f'{instance} has no attribute {self._name}'
                )
    
            return self._value
    
        def __set_name__(self, owner, name):
            self._name = name
    
    
    class_only = ClassOnlyDescriptor
    
    
    class A:
        x = class_only(2)
    
    
    print(A.x)  # 2
    A().x       # raises AttributeError
    

    См. также, как работает Django-декоратор classonlymethod: https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6


    Функциям, объявленным в теле класса, область видимости этого класса недоступна. Это сделано потому, что эта область видимости существует только в ходе создания класса.

    >>> class A:
    ...     x = 2
    ...     def f():
    ...         print(x)
    ...     f()
    ...
    [...]
    NameError: name 'x' is not defined

    Обычно это не является проблемой: методы объявляются внутри класса только для того, чтобы стать методами и быть вызванными позднее:

    >>> class A:
    ...     x = 2
    ...     def f(self):
    ...         print(self.x)
    ...
    >>>
    >>>
    >>> A().f()
    2

    Удивительно, но то же самое верно и для comprehensions. У них собственные области видимости и они тоже не имеют доступа к областям видимости классов. Это очень логично с точки зрения generator comprehensions: код в них исполняется, когда класс уже создан.

    >>> class A:
    ...     x = 2
    ...     y = [x for _ in range(5)]
    ...
    [...]
    NameError: name 'x' is not defined

    Однако у comprehensions нет доступа к self. Единственный способ обеспечить доступ к x заключается в добавлении ещё одной области видимости (ага, дурацкое решение):

    >>> class A:
    ...     x = 2
    ...     y = (lambda x=x: [x for _ in range(5)])()
    ...
    >>> A.y
    [2, 2, 2, 2, 2]
    


    В Python None эквивалентно None, так что может показаться, что проверять на None можно с помощью ==:

    ES_TAILS = ('s', 'x', 'z', 'ch', 'sh')
    
    
    def make_plural(word, exceptions=None):
        if exceptions == None:  # ← ← ←
            exceptions = {}
    
        if word in exceptions:
            return exceptions[word]
        elif any(word.endswith(t) for t in ES_TAILS):
            return word + 'es'
        elif word.endswith('y'):
            return word[0:-1] + 'ies'
        else:
            return word + 's'
    
    exceptions = dict(
        mouse='mice',
    )
    
    print(make_plural('python'))
    print(make_plural('bash'))
    print(make_plural('ruby'))
    print(make_plural('mouse', exceptions=exceptions))
    

    Но это будет ошибкой. Да, None равно None, но не только оно. Пользовательские объекты тоже могут быть равны None:

    >>> class A:
    ...     def __eq__(self, other):
    ...             return True
    ...
    >>> A() == None
    True
    >>> A() is None
    False

    Единственный правильный способ сравнения с None заключается в использовании is None.



    Числа с плавающей запятой в Python могут иметь значения NaN. Например, такое число можно получить с помощью math.nan. nan не равно ничему, включая себя:

    >>> math.nan == math.nan
    False

    Кроме того, NaN-объект не уникален, у вас может быть несколько разных NaN-объектов из разных источников:

    >>> float('nan')
    nan
    >>> float('nan') is float('nan')
    False

    Это означает, что, в целом, вы не можете использовать NaN в качестве ключа словаря:

    >>> d = {}
    >>> d[float('nan')] = 1
    >>> d[float('nan')] = 2
    >>> d
    {nan: 1, nan: 2}
    


    typing позволяет определять типы для генераторов. Дополнительно можно указать, какой тип генерируется, какой передаётся генератору и какой возвращается с помощью return. Например, Generator[int, None, bool] генерирует целые числа, возвращает булевы и не поддерживает g.send().

    А вот пример посложнее. chain_while проксирует данные от других генераторов до тех пор, пока один из них не вернёт значение, которое является сигналом остановки в соответствии с функцией condition:

    from typing import Generator, Callable, Iterable, TypeVar
    
    Y = TypeVar('Y')
    S = TypeVar('S')
    R = TypeVar('R')
    
    
    def chain_while(
        iterables: Iterable[Generator[Y, S, R]],
        condition: Callable[[R], bool],
    ) -> Generator[Y, S, None]:
        for it in iterables:
            result = yield from it
            if not condition(result):
                break
    
    
    def r(x: int) -> Generator[int, None, bool]:
        yield from range(x)
        return x % 2 == 1
    
    
    print(list(chain_while(
        [
            r(5),
            r(4),
            r(3),
        ],
        lambda x: x is True,
    )))
    


    Задать аннотации для фабричного метода не так просто, как может показаться. Сразу хочется использовать нечто подобное:

    class A:
        @classmethod
        def create(cls) -> 'A':
            return cls()

    Но это будет неправильно. Хитрость в том, что create возвращает не A, он возвращает cls, который является A или одним из его потомков. Взгляните на код:

    class A:
        @classmethod
        def create(cls) -> 'A':
            return cls()
    
    
    class B(A):
        @classmethod
        def create(cls) -> 'B':
            return super().create()

    Результатом проверки mypy является ошибка error: Incompatible return value type (got "A", expected "B"). Повторюсь, проблема в том, что super().create() аннотирован как возвращающий A, хотя в этом случае он возвращает B.

    Это можно исправить, если аннотировать cls с помощью TypeVar:

    AType = TypeVar('AType')
    BType = TypeVar('BType')
    
    
    class A:
        @classmethod
        def create(cls: Type[AType]) -> AType:
            return cls()
    
    
    class B(A):
        @classmethod
        def create(cls: Type[BType]) -> BType:
            return super().create()

    Теперь create возвращает экземпляр класса cls. Однако эти аннотации слишком расплывчаты, мы потеряли информацию о том, что cls является подтипом A:

    AType = TypeVar('AType')
    
    
    class A:
        DATA = 42
        @classmethod
        def create(cls: Type[AType]) -> AType:
            print(cls.DATA)
            return cls()

    Получаем ошибку "Type[AType]" has no attribute "DATA".

    Чтобы её исправить, нужно явно определить AType как подтип A. Для этого используется TypeVar с аргументом bound.

    AType = TypeVar('AType', bound='A')
    BType = TypeVar('BType', bound='B')
    
    
    class A:
        DATA = 42
        @classmethod
        def create(cls: Type[AType]) -> AType:
            print(cls.DATA)
            return cls()
    
    
    class B(A):
        @classmethod
        def create(cls: Type[BType]) -> BType:
            return super().create()
    
    • +49
    • 6,5k
    • 9
    Mail.ru Group
    802,10
    Строим Интернет
    Поделиться публикацией

    Похожие публикации

    Комментарии 9

      0
      Аннотации типов помогают делать очень интересные вещи. Довольно давно начал их активно использовать, но вот с TypeVar познакомился совсем недавно.
      А еще аннотации можно использовать не только по прямому назначению — для типизации.

      Например, они мне очень пригодились при построении одного из моих велосипедов — маппера из JSON в объекты Python. По началу я использовал для описания структур обычный dict, но пострадало удобство — IDE ничего не знает о полях соответствующих объектов. Привет опечатки и постоянное заглядывание в описания объектов. Тут-то на помощь и пришли аннотации и немного нестандартное их использование. И вуаля, IDE подсказывает мне поля и следит за их типами. Если кому интересно, могу рассказать подробнее.
        0

        Интересно! Рассказывайте

        0

        Пример с nan не воспроизвёлся:


        >>> import math
        >>> d = {}
        >>> d[math.nan] = 1
        >>> d[math.nan] = 2
        >>> d
        {nan: 2}
        >>>

        Да и вообще,


        >>> math.nan is math.nan
        True

        ЧЯДНТ? Win10, Python 3.6

          0
          NaN — не синглтон, math.nan — это просто какой-то один экземпляр, можно легко получить другой. С помощью float('nan'), например.
          0
          По поводу аннотации для фабричного метода — c 3.7 можно использовать
          from __future__ import annotations

          И не придётся указывать TypeVar.

          PEP563
            0
            А можно пример?
              0
              Вроде как-то так:
              from __future__ import annotations
              from typing import Type
              
              
              class A:
                  DATA = 42
                  @classmethod
                  def create(cls: Type[A]) -> A:
                      print(cls.DATA)
                      return cls()
              
                0
                Это позволяет не ставить кавычки вокруг 'A', но вообще никак не относится к описанной проблеме и TypeVar не заменяет.
                  0
                  Да, вроде понял, спасибо!

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое