📜 ⬆️ ⬇️

@Pythonetc compilation, 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 to create an object use the factory method. This will separate the business logic from the technical implementation of the creation of objects.

In this example, __init__ accepts host and port as arguments to the database:

 class Query: def __init__(self, host, port): self._connection = Connection(host, port) 

Refactoring option:

 class Query: def __init__(self, connection): self._connection = connection @classmethod def create(cls, host, port): return cls(Connection(host, port)) 

This approach has at least the following advantages:


super or next


The super() function 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 redefining it completely.

 class BaseTestCase(TestCase): def setUp(self): self._db = create_db() class UserTestCase(BaseTestCase): def setUp(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”). At the same time, 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.

 class Top: def foo(self): return 'top' class Left(Top): def foo(self): return super().foo() class Right(Top): def foo(self): return 'right' class Bottom(Left, Right): pass # prints 'right' print(Bottom().foo()) 

Do not forget that super() can 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 by locals() ) is used by the metaclass (by default it is type ) to create a class object.

 class Meta(type): def __new__(meta, name, bases, ns): print(ns) return super().__new__( meta, name, bases, ns ) class Foo(metaclass=Meta): B = 2 

This code displays {'__module__': '__main__', '__qualname__':'Foo', 'B': 3} .

Obviously, if we introduce something like B = 2; B = 3 B = 2; B = 3 , then the metaclass will see only 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, similar to a dictionary, if you use the metaclass __prepare__ method.

 class CustomNamespace(dict): def __setitem__(self, key, value): print(f'{key} -> {value}') return super().__setitem__(key, value) class Meta(type): def __new__(meta, name, bases, ns): return super().__new__( meta, name, bases, ns ) @classmethod def __prepare__(metacls, cls, bases): return CustomNamespace() class Foo(metaclass=Meta): B = 2 B = 3 

The result of the code:

 __module__ -> __main__ __qualname__ -> Foo B -> 2 B -> 3 

Thus, enum.Enum protected from duplication .

matplotlib


matplotlib is a complex and flexible Python library for graphing. 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 datetime library for working with dates and times. It is curious that datetime objects have a special interface for supporting time zones (namely, the tzinfo attribute), 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 pytz does not fully match the tzinfo interface. This is stated at the very beginning of the pytz documentation: “This library differs from the documented Python API for tzinfo implementations.”

You cannot use time zone pytz objects as tzinfo . 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 to avoid changing the offsets (for example, on 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 of pytz . This library is fully compatible with tzinfo , it can be passed as an attribute and you do not need to use normalize . True, it works more slowly.

If you want to know why pytz does not support the datetime API, or want to see more examples, then read this article.

Magic StopIteration


Each time next(x) called, it returns a new value from the iterator x until an exception is thrown. If it turns out to be StopIteration , then 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 throw a StopIteration :

 >>> def one_two(): ... yield 1 ... yield 2 ... >>> 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 cause next :

 >>> list(one_two()) [1, 2] 

But the problem is that any obviously not expected StopIteration that 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:

 def one_two(): yield 1 yield 2 def one_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 last yield is an error: the thrown StopIteration exception stops iterating the list(...) . We get the result [1, 2] . However, in Python 3.7, this behavior has changed. Alien 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 the __future__ import generator_stop to enable the same behavior since Python 3.5.

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


All Articles