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






All Articles