@Pythonetc November 2018 Compilation

This is the sixth collection of tips on Python and programming from my author’s @pythonetc channel.
Previous selections:
Atypical decorators
Function decorators are not required to return only new functions, they can return any other value:
defcall(*args, **kwargs):defdecorator(func):return func(*args, **kwargs)
return decorator
@call(15)defsqr_15(x):return x * x
assert sqr_15 == 225
This is useful for creating simple classes with only one override method:
from abc import ABCMeta, abstractmethod
classBinaryOperation(metaclass=ABCMeta):def__init__(self, left, right):
self._left = left
self._right = right
def__repr__(self):
klass = type(self).__name__
left = self._left
right = self._right
returnf'{klass}({left}, {right})' @abstractmethoddefdo(self):pass @classmethoddefmake(cls, do_function):return type(
do_function.__name__,
(BinaryOperation,),
dict(do=do_function),
)
classAddition(BinaryOperation):defdo(self):return self._left + self._right
@BinaryOperation.makedefSubtraction(self):return self._left - self._right
__length_hint__
The PEP 424 allows generators and other iterable objects that do not have a specific predetermined size to return their approximate length. For example, this generator will certainly return about 50 items:
(x for x in range(100) if random() > 0.5)
If you are writing something iterable and want to return an approximate length, then define a method
__length_hint__
. And if you exactly know the length, then use __len__
. If you are using an iterated object and want to know how long it can be, use operator.length_hint
.in with generator
The operator
in
can be used with generators: x in g
. In this case, Python will be iterated on g
until it is found x
or until it ends g
.>>> defg():... print(1)
... yield1... print(2)
... yield2... print(3)
... yield3
...
>>> 2in g()
12True
range()
however, it works somewhat better. It has a magic redefined method __contains__
, due to which the computational complexity in
becomes equal to O (1):In [1]: %timeit 10**20in range(10**30)
375 ns ± 10.7 ns per loop
Please note that
xrange()
this will not work with the function from Python 2.Operators + = and +
In Python, there are two different operators:
+=
and +
. Methods are responsible for their behavior __iadd__
and, __add__
accordingly.classA:def__init__(self, x):
self.x = x
def__iadd__(self, another):
self.x += another.x
return self
def__add__(self, another):return type(self)(self.x + another.x)
If
__iadd__
not defined, it a += b
will work as a = a + b
. The semantic difference between
+=
and +
lies in the fact that the first changes the object, and the second creates a new one:>>> a = [1, 2, 3]
>>> b = a
>>> a += [4]
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 4]
>>> a = a + [5]
>>> a
[1, 2, 3, 4, 5]
>>> b
[1, 2, 3, 4]
Function as an attribute of a class
You cannot store a function as a class attribute, because it will be automatically converted to a method if it is accessed through an instance:
>>> classA:... CALLBACK = lambda x: x ** x
...
>>> A.CALLBACK
<function A.<lambda> at 0x7f68b01ab6a8>
>>> A().CALLBACK
<bound method A.<lambda> of <__main__.A object at 0x7f68b01aea20>>
>>> A().CALLBACK(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes 1 positional argument but 2 were given
You can cheat and wrap the function in a trivial descriptor:
>>> classFunctionHolder:... def__init__(self, f):... self._f = f
... def__get__(self, obj, objtype):... return self._f
...
>>> classA:... CALLBACK = FunctionHolder(lambda x: x ** x)
...
>>> A().CALLBACK
<function A.<lambda> at 0x7f68b01ab950>
You can also get out of the situation by using the class method instead of an attribute.
classA: @classmethoddef_get_callback(cls):returnlambda x: x ** x