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 hostand 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 hostand port, 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 = 3that, 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.Enumprotected 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 datetimefor working with dates and times. It is curious that objects datetimehave 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 pytzdoes 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 pytztime zone objects as the tzinfoattributes. 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 normalizeto 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.tzinstead 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 pytzdoes 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 xnew 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 yieldis an error: the thrown exception StopIterationstops the iteration list(...). We get the result [1, 2]. However, in Python 3.7, this behavior has changed. Alien was StopIterationreplaced 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.

    Also popular now: