Selection @pythonetc, August 2018

This is the third collection of tips on Python and programming from my author’s @pythonetc channel .
Previous selections:
Factory Method
If you create new objects inside
__init__
, then it is more expedient to pass them ready as arguments, and use the factory method to create an object. This will separate the business logic from the technical implementation of the creation of objects. In this example,
__init__
to create a connection to the database takes as arguments host
and port
:classQuery:def__init__(self, host, port):
self._connection = Connection(host, port)
Refactoring option:
classQuery:def__init__(self, connection):
self._connection = connection
@classmethoddefcreate(cls, host, port):return cls(Connection(host, port))
This approach has at least the following advantages:
- Easy to implement dependencies. In the tests can be done
Query(FakeConnection())
. - A class can have as many factory methods as you want. You can create a connection not only with the help of
host
andport
, but also by cloning another connection, reading the configuration file, using the default connection, etc. - Such factory methods can be turned into asynchronous functions, which is absolutely impossible to turn with
__init__
.
super or next
The function
super()
allows you to refer to the base class. This is very useful in cases where the derived class wants to add something to the implementation of the method, rather than overriding it completely.classBaseTestCase(TestCase):defsetUp(self):
self._db = create_db()
classUserTestCase(BaseTestCase):defsetUp(self):
super().setUp()
self._user = create_user()
The name super does not mean anything "super". In this context, it means “higher in the hierarchy” (for example, as in the word “superintendent”). It
super()
does not always refer to the base class, it can easily return a child class. So it would be better to use the name next()
, since the next class is returned according to the MRO.classTop:deffoo(self):return'top'classLeft(Top):deffoo(self):return super().foo()
classRight(Top):deffoo(self):return'right'classBottom(Left, Right):pass# prints 'right'
print(Bottom().foo())
Do not forget that it
super()
may produce different results depending on where the method was originally called from.>>> Bottom().foo()
'right'>>> Left().foo()
'top'
Custom namespace to create class
The class is created in two big stages. First, the class body is executed as the body of a function. In the second stage, the resulting namespace (which is returned
locals()
) is used by the metaclass (the default is type
) to create a class object.classMeta(type):def__new__(meta, name, bases, ns):
print(ns)
return super().__new__(
meta, name,
bases, ns
)
classFoo(metaclass=Meta):
B = 2
This code displays on the screen
{'__module__': '__main__', '__qualname__':'Foo', 'B': 3}
. Obviously, if you type in something like
B = 2; B = 3
that, then the metaclass will only see B = 3
, because only this value is in ns
. This limitation stems from the fact that the metaclass begins to work only after the body is executed. However, you can intervene in the execution procedure by slipping your own namespace. By default, a simple dictionary is used, but you can provide your own object that looks like a dictionary if you use the method
__prepare__
from the metaclass.classCustomNamespace(dict):def__setitem__(self, key, value):
print(f'{key} -> {value}')
return super().__setitem__(key, value)
classMeta(type):def__new__(meta, name, bases, ns):return super().__new__(
meta, name,
bases, ns
)
@classmethoddef__prepare__(metacls, cls, bases):return CustomNamespace()
classFoo(metaclass=Meta):
B = 2
B = 3
The result of the code:
__module__ -> __main__
__qualname__ -> Foo
B -> 2
B -> 3
This way it is
enum.Enum
protected from duplication .matplotlib
matplotlib
- difficult and flexible in application Python-library for an output of schedules. It is supported by many products, including Jupyter and Pycharm. Here is an example of drawing a simple fractal using matplotlib
: https://repl.it/@VadimPushtaev/myplotlib (see the title picture of this publication).Time zone support
Python provides a powerful library
datetime
for working with dates and times. It is curious that objects datetime
have a special interface for supporting time zones (namely, an attribute tzinfo
), but this module has limited support for the interface, so some of the work is assigned to other modules. The most popular of them is
pytz
. But the fact is that it pytz
does not fully comply with the interface tzinfo
. This is stated at the very beginning of the documentation pytz
: “This library differs from the documented Python API for tzinfo implementations.” You cannot use the
pytz
time zone objects as the tzinfo
attributes. If you try to do it, you risk getting a completely insane result:In : paris = pytz.timezone('Europe/Paris')
In : str(datetime(2017, 1, 1, tzinfo=paris))
Out: '2017-01-01 00:00:00+00:09'
Pay attention to the offset +00: 09. Pytz should be used like this:
In : str(paris.localize(datetime(2017, 1, 1)))
Out: '2017-01-01 00:00:00+01:00'
In addition, after any arithmetic operations, you need to apply
normalize
to your datetime
-objects in order to avoid changes in the offsets (for example, at the border of the DST-period).In : new_time = time + timedelta(days=2)
In : str(new_time)
Out: '2018-03-27 00:00:00+01:00'
In : str(paris.normalize(new_time))
Out: '2018-03-27 01:00:00+02:00'
If you have Python 3.6, the documentation recommends using
dateutil.tz
instead pytz
. This library is fully compatible with tzinfo
, it can be passed as an attribute and does not need to be applied normalize
. True, it works more slowly. If you want to know why it
pytz
does not support the API datetime
, or if you want to see more examples, then read this article.Magic StopIteration
On each call,
next(x)
returns a x
new value from the iterator until an exception is thrown. If this happens StopIteration
, the iterator is exhausted and can no longer provide values. If the generator is iterated, then at the end of the body it will automatically cast StopIteration
:>>> defone_two():... yield1... yield2
...
>>> i = one_two()
>>> next(i)
1>>> next(i)
2>>> next(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
StopIteration
can be automatically processed with tools that call next
:>>> list(one_two())
[1, 2]
But the problem is that any obviously not expected StopIteration, which occurred in the generator body, will be silently taken as a sign of the end of the generator, and not an error, like any other exception:
defone_two():yield1yield2defone_two_repeat(n):for _ in range(n):
i = one_two()
yield next(i)
yield next(i)
yield next(i)
print(list(one_two_repeat(3)))
Here the latter
yield
is an error: the thrown exception StopIteration
stops the iteration list(...)
. We get the result [1, 2]
. However, in Python 3.7, this behavior has changed. Alien was StopIteration
replaced by RuntimeError
:Traceback (most recent call last):
File "test.py", line 10, in one_two_repeat
yield next(i)
StopIteration
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 12, in <module>
print(list(one_two_repeat(3)))
RuntimeError: generator raised StopIteration
You can use it
__future__ import generator_stop
to enable the same behavior since Python 3.5.