@Pythonetc compilation, august 2019



A new selection of Python tips and programming from my @pythonetc feed.

Previous collections


If the class instance does not have an attribute with the given name, then it tries to access the class attribute with the same name.

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

An instance can easily have an attribute that the class does not have, or have an attribute with a different value:

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

If you want the instance to behave as if it does not have an attribute, although the class has it, then you will have to create a custom handle that prohibits access from this 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(Ax) # 2 A().x # raises AttributeError 

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


Functions declared in the class body are not accessible to the scope of this class. This is because this scope exists only during the creation of the class.

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

This is usually not a problem: methods are declared inside the class just to become methods and be called later:

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

Surprisingly, the same is true for comprehensions. They have their own scope and they also do not have access to the scope of the classes. This is very logical from the point of view of generator comprehensions: the code in them is executed when the class is already created.

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

However, comprehensions do not have access to self . The only way to provide access to x is to add another scope (yeah, stupid solution):

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


In Python, None equivalent to None , so it might seem 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)) 

But that will be a mistake. Yes, None is equal to None , but not only that. Custom objects can also be None :

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

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



Floating point numbers in Python can have NaN values. For example, such a number can be obtained using math.nan . nan not equal to anything, including itself:

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

In addition, a NaN object is not unique; you can have several different NaN objects from different sources:

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

This means that, in general, you cannot 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 types for generators. Additionally, you can specify which type is generated, which is passed to the generator, and which is returned using return . For example, Generator[int, None, bool] generates integers, returns Booleans, and does not support g.send() .

But the example is more complicated. chain_while data from other generators until one of them returns a value that is a stop signal in accordance with 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, ))) 


Setting annotations for a factory method is not as easy as it might seem. Just want to use something like this:

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

But it will be wrong. The trick is that create returns not A , it returns cls , which is A or one of its descendants. Take a look at the code:

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

The result of the mypy check is the error error: Incompatible return value type (got "A", expected "B") . Once again, the problem is that super().create() annotated as returning A , although in this case it returns B

This can be fixed if annotating cls using 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 now returns an instance of the cls class. However, these annotations are too vague, we have lost 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() 

We "Type[AType]" has no attribute "DATA" error "Type[AType]" has no attribute "DATA" .

To fix it, you must explicitly define AType as a subtype of A For this, TypeVar with the bound argument is used.

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

Source: https://habr.com/ru/post/466315/


All Articles