Esta é a terceira coleção de dicas e programação em Python do meu
feed @pythonetc .
Seleções anteriores:
Método de fábrica
Se você criar novos objetos dentro de
__init__
, será mais conveniente deixá-los prontos como argumentos e usar o método de fábrica para criar o objeto. Isso separará a lógica de negócios da implementação técnica da criação de objetos.
Neste exemplo,
__init__
aceita
host
e
port
como argumentos para criar uma conexão com o banco de dados:
class Query: def __init__(self, host, port): self._connection = Connection(host, port)
Opção de refatoração:
class Query: def __init__(self, connection): self._connection = connection @classmethod def create(cls, host, port): return cls(Connection(host, port))
Essa abordagem possui pelo menos as seguintes vantagens:
- A implantação é fácil. Nos testes, você pode fazer a
Query(FakeConnection())
. - Uma classe pode ter quantos métodos de fábrica você desejar. Você pode criar uma conexão não apenas usando
host
e port
, mas também clonando outra conexão, lendo o arquivo de configuração, usando a conexão padrão etc. - Métodos de fábrica similares podem ser transformados em funções assíncronas, o que é absolutamente impossível de fazer com
__init__
.
super ou próximo
A função
super()
permite que você faça referência à classe base. Isso pode ser muito útil nos casos em que a classe derivada deseja adicionar algo à implementação do método, em vez de substituí-lo completamente.
class BaseTestCase(TestCase): def setUp(self): self._db = create_db() class UserTestCase(BaseTestCase): def setUp(self): super().setUp() self._user = create_user()
O nome super não significa nada "super". Nesse contexto, significa "mais alto na hierarquia" (por exemplo, como na palavra "superintendente"). Ao mesmo tempo,
super()
nem sempre se refere à classe base; ele pode retornar facilmente uma classe filho. Portanto, seria mais correto usar o nome
next()
, pois a próxima classe é retornada de acordo com o 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
Lembre-se de que
super()
pode produzir resultados diferentes, dependendo de onde o método foi originalmente chamado.
>>> Bottom().foo() 'right' >>> Left().foo() 'top'
Namespace personalizado para criar uma classe
Uma classe é criada em duas grandes etapas. Primeiro, o corpo da classe é executado, como o corpo de uma função. Na segunda etapa, o espaço para nome resultante (retornado por
locals()
) é usado pela metaclasse (o padrão é o
type
) para criar um objeto da classe.
class Meta(type): def __new__(meta, name, bases, ns): print(ns) return super().__new__( meta, name, bases, ns ) class Foo(metaclass=Meta): B = 2
Este código exibe
{'__module__': '__main__', '__qualname__':'Foo', 'B': 3}
.
Obviamente, se você introduzir algo como
B = 2; B = 3
B = 2; B = 3
, a metaclasse verá apenas
B = 3
, porque apenas esse valor está em
ns
. Essa limitação decorre do fato de que a metaclasse começa a funcionar somente após a execução do corpo.
No entanto, você pode intervir no procedimento de execução deslizando seu próprio espaço para nome. Um dicionário simples é usado por padrão, mas você pode fornecer seu próprio objeto, semelhante a um dicionário, se você usar o método
__prepare__
da metaclasse.
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
Resultado da execução do código:
__module__ -> __main__ __qualname__ -> Foo B -> 2 B -> 3
Assim, o
enum.Enum
protegido contra
duplicação .
matplotlib
matplotlib
é uma biblioteca Python complexa e flexível para gráficos. Muitos produtos suportam, incluindo Jupyter e Pycharm. Aqui está um exemplo de renderização de um fractal simples usando
matplotlib
:
https://repl.it/@VadimPushtaev/myplotlib (veja a imagem do título desta publicação).
Suporte a fuso horário
O Python fornece uma poderosa biblioteca de data e
datetime
para trabalhar com datas e horas. É interessante que os objetos de
datetime
tenham uma interface especial para suportar fusos horários (ou seja, o atributo
tzinfo
), mas este módulo possui suporte limitado para a interface mencionada, portanto parte do trabalho é atribuída a outros módulos.
O mais popular deles é o
pytz
. Mas o fato é que o
pytz
não está totalmente em conformidade com a interface
tzinfo
. Isso é afirmado no início da documentação do
pytz
: "Esta biblioteca difere da API do Python documentada para implementações do tzinfo".
Você não pode usar objetos
pytz
como
tzinfo
. Se você tentar fazer isso, corre o risco de obter um resultado completamente insano:
In : paris = pytz.timezone('Europe/Paris') In : str(datetime(2017, 1, 1, tzinfo=paris)) Out: '2017-01-01 00:00:00+00:09'
Observe o deslocamento +00: 09. Pytz deve ser usado assim:
In : str(paris.localize(datetime(2017, 1, 1))) Out: '2017-01-01 00:00:00+01:00'
Além disso, após qualquer operação aritmética, é necessário aplicar a
normalize
aos objetos de
datetime
para evitar alterações de compensações (por exemplo, na fronteira do período do horário de verão).
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'
Se você possui o Python 3.6, a documentação recomenda o uso de
dateutil.tz
vez de
pytz
. Esta biblioteca é totalmente compatível com o
tzinfo
, pode ser transmitida como um atributo e você não precisa usar a
normalize
. É verdade que funciona mais devagar.
Se você deseja saber por que o
pytz
não suporta a API
datetime
ou deseja ver mais exemplos, leia
este artigo.
Magic StopIteration
Cada vez que o
next(x)
chamado, ele retorna um novo valor do iterador
x
até que uma exceção seja lançada. Se for
StopIteration
, o iterador estará esgotado e não poderá mais fornecer valores. Se o gerador for iterado, no final do corpo ele lançará automaticamente
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
pode ser manipulado automaticamente por ferramentas que chamam a
next
:
>>> list(one_two()) [1, 2]
Mas o problema é que qualquer StopIteration obviamente não esperada que surgir no corpo do gerador será silenciosamente aceita como um sinal do fim do gerador e não como um erro, como qualquer outra exceção:
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)))
Aqui, o último
yield
é um erro: uma exceção
StopIteration
lançada interrompe a iteração da
list(...)
. Nós obtemos o resultado
[1, 2]
. No entanto, no Python 3.7 esse comportamento foi alterado. Alien
StopIteration
substituído por
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
Você pode usar
__future__ import generator_stop
para ativar o mesmo comportamento desde o Python 3.5.