Mail.ru Group corporate blog
Python
Programming
5 September

Tips and tricks from my Telegram-channel @pythonetc, August 2019



It is a new selection of tips and tricks about Python and programming from my Telegram-channel @pythonetc.

Previous publications


If an instance of a class doesn’t have an attribute with the given name, it tries to access the class attribute with the same name.

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

It’s fairly simple for an instance to have attribute that a class doesn’t or have the attribute with the different value:

>>> 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'

If it’s not that simple, however, if you want an instance behave like it doesn’t have an attribute despite the class having it. To make it happen you have to create custom descriptor that doesn’t allow access from the instance:

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

See also how the Django classonlymethod decorator works: https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6


Functions declared in a class body can’t see the class scope. It makes sense since the class scope only exists during class creation.

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

That is usually not a problem: methods are declared inside a class only to become methods and be called later:

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

Somewhat surprisingly, the same is true for comprehensions. They have their own scopes and can’t access the class scope as well. That really make sense for generator comprehensions: they evaluate expressions after the class creation is already finished.

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

Comprehensions, however, have no access to self. The only way to make it work is to add one more scope (yep, that’s ugly):

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



In Python, None is equal to None so it looks like you can check for None with ==:

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

This is a wrong thing to do though. None is indeed is equal to None, but it’s not the only thing that is. Custom objects may be equal to None too:

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

The only proper way to compare with None is to use is None.


Python floats can have NaN values. You can get one with math.nan. nan is not equal to anything including itself:

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

Also, NaN object is not unique, you can have several different NaN objects from different sources:

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

That means that you generally can’t use NaN as a dictionary key:

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


typing allows you to define type for generators. You can additionally specify what type is yielded, what type can be sent into a generator and what is returned. Generator[int, None, bool] is a generator that yields integers, returns boolean value and doesn’t support g.send().

Here is slightly more complicated example. chain_while yields from other generators until one of them returns something that is a signal to stop according to the condition function:

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,
)))


Annotating a factory method is not as simple as it may seem. The immediate urge is to use something like this:

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

However, that is not a right thing to do. The catch is, create doesn’t return A, it returns an instance of cls that is A or any of its descendants. Look at this code:

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


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

The mypy check result is error: Incompatible return value type (got "A", expected "B"). Again, the problem is super().create() is annotated to return A while it clearly returns B in this case.

You can fix that by annotating cls with 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()

Now create returns the instance of the cls class. However, this annotations are too loose, we lost the information that cls is a subtype of A:

AType = TypeVar('AType')


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

The error is "Type[AType]" has no attribute "DATA".

To fix that you have to explicitly define AType as a subtype of A with the bound argument of TypeVar:

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()

+25
1.1k 0
Comments 1
Top of the day