Apresentando o Python para camaradas superando a “linguagem A vs. V” língua B "e outros preconceitos

Para todos os residentes de habrach que têm o sentimento de déjà vu: fui solicitado a escrever este post pelo artigo "Introdução ao Python" e comentá-lo. Infelizmente, a qualidade desta "introdução" ahem ... não vamos falar de coisas tristes. Mas foi ainda mais triste observar brigas nos comentários, da categoria “C ++ é mais rápido que Python”, “Ferrugem é ainda mais rápida que C ++”, “Python não é necessário” etc. É incrível que eles não se lembrassem de Ruby!


Como Bjarn Stroustrup disse,


"Existem apenas dois tipos de linguagens de programação: aquelas que as pessoas juram o tempo todo e aquelas que ninguém usa".

Bem-vindo a todos que gostariam de se familiarizar com o Python sem cair em maldições sujas!


A manhã nas montanhas do Cáucaso Oriental foi marcada por gritos. Dois jovens sentaram-se em uma pedra grande e discutiram vigorosamente algo, gesticulando ativamente. Um minuto depois, eles começaram a se empurrar, e então se agarraram e caíram de uma pedra em (como se viu) um arbusto de urtiga. Aparentemente, esse arbusto cresceu lá por uma razão - ele imediatamente pacificou os brigões e trouxe uma trégua à sua disputa insaciável. Como você provavelmente adivinhou, eu era um dos debatedores, o outro era meu melhor amigo (olá, Quaker_t!), Mas o assunto da nossa conversa foi Visual Basic vs. Delphi !


Você se reconhece? Às vezes, transformamos nossas linguagens de programação favoritas em um culto e estamos prontos para defendê-la até o fim! Mas os anos passam e chega o momento em que "A vs. B" do assunto das disputas se transforma em "Estou mais confortável trabalhando com A, mas, se necessário, aprenderei a trabalhar com B, C, D, E e, em geral, com qualquer coisa ". Somente quando nos deparamos com novas linguagens de programação, velhos hábitos e cultura podem não nos deixar sair por um longo tempo.


Gostaria de apresentar o Python e ajudar a transferir sua experiência para uma nova direção. Como qualquer tecnologia, ela tem seus próprios pontos fortes e fracos. Python, como C ++, Rust, Ruby, JS e todos os outros, é uma ferramenta. As instruções estão anexadas a qualquer instrumento e você deve aprender a usá-lo corretamente .


"Autor, não tem cérebro, você iria nos apresentar o Python?" Vamos nos familiarizar!


Python é uma linguagem de programação dinâmica e de alto nível para fins gerais. Python é uma linguagem de programação madura , com um rico ecossistema e tradição. Embora a linguagem tenha sido lançada em 1991, sua aparência moderna começou a tomar forma no início dos anos 2000. Python é uma linguagem carregada , em sua biblioteca padrão existem soluções para muitas ocasiões. Python é uma linguagem de programação popular : Dropbox, Reddit, Instagram, Disqus, YouTube, Netflix, droga, até Eve Online e muitas outras usam ativamente o Python.


Qual o motivo dessa popularidade? Com sua permissão, apresentarei minha própria versão.


Python é uma linguagem de programação simples . Digitação dinâmica. Coletor de lixo. Funções de ordem superior. Sintaxe simples para trabalhar com dicionários, conjuntos, tuplas e listas (inclusive para obter fatias). O Python é ótimo para iniciantes: torna possível começar com programação procedural, alternar lentamente para OOP e experimentar a programação funcional. Mas essa simplicidade é como a ponta de um iceberg. Vale a pena mergulhar nas profundezas quando você se deparar com a filosofia do Python - Zen Of Python . Mergulhando ainda mais - e você se encontra em um conjunto de regras claras para o design de código - Guia de Estilo para Código Python . Mergulhando, o programador aprofunda-se gradualmente no conceito de "caminho Python" ou "Pythonic". Nesse estágio incrível do aprendizado de idiomas, você começa a entender por que bons programas Python são escritos dessa maneira e não de outra forma. Por que a linguagem evoluiu nessa direção, e não em outra. Python não teve sucesso em velocidade. Mas ele conseguiu o aspecto mais importante do nosso trabalho - a legibilidade. "Escreva código para pessoas, não para carros" - esta é a base do básico do Python.


Um bom código Python está bonito. E para escrever um código bonito - o que não é uma ocupação agradável?


Dica 0: Antes de ler mais, dê uma olhada em um canto do Zen Python . O idioma é baseado nesses postulados e nossa comunicação será muito mais agradável se você estiver familiarizado com eles.


Que homem inteligente surgiu com indentação?


O primeiro choque para quem nunca viu o código em Python é o recuo do corpo de instruções:


def main(): ins = input('Please say something') for w in ins.split(' '): if w == 'hello': print('world!') 

Lembro-me das noites no albergue do Politécnico de Petersburgo quando meu vizinho, VlK , com olhos ardentes me disse que havia descoberto algo novo em Python. "Corpo de indentação? Sério?" - foi a minha reação. De fato, para uma pessoa que passou do Visual Basic ( if ... end if ) para C # (chaves) através de C, C ++ e Java, essa abordagem parecia, para dizer o mínimo, estranha. "Você está formatando o código com indentação?", Perguntou VlK . Claro que eu o formatei. Mais precisamente, o Studio Visual espiral fez isso por mim. Ela fez isso muito bem. Eu nunca pensei em formatação e recuo - eles apareceram no código sozinhos e pareciam algo comum e familiar. Mas não havia nada a esconder - o código sempre era formatado com recuo. "Então por que você precisa de aparelho se o corpo das instruções é, de qualquer forma, deslocado para a direita?"


Naquela noite, sentei-me com Python. Olhando para trás, posso dizer com certeza o que exatamente ajudou a absorver rapidamente o novo material. Era um editor de código. Influenciado pelo mesmo VlK , pouco antes dos eventos descritos acima, mudei do Windows para o Ubuntu e o Emacs como editor (no pátio de 2007, para PyCharm, Atom, VS Code e outros - muitos mais anos). "Bem, agora o Emacs irá PR ..." - você diz. Só um pouquinho :) Tradicionalmente, a tecla <tab> no Emacs não adiciona tabulações, mas serve para alinhar a linha de acordo com as regras deste modo. Pressione <tab> - e a linha de código é deslocada para a próxima posição apropriada:



Dessa forma, você nunca precisará pensar se alinhara o código corretamente.


Dica 1: Ao conhecer o Python, use um editor que cuide do recuo.


Você sabe que efeito colateral toda essa desgraça tem? O programador tenta evitar construções longas. Assim que o tamanho da função ultrapassa as bordas verticais da tela, fica mais difícil distinguir a qual design o bloco de código especificado pertence. E quanto mais investimentos, mais difícil. Como resultado, você tenta escrever da forma mais concisa possível, dividindo os longos corpos de funções, loops, transições condicionais etc.


Oh, bem, sua digitação dinâmica


O, essa discussão existe quase enquanto existir o conceito de “programação”! A digitação dinâmica não é ruim nem boa. A digitação dinâmica também é nossa ferramenta. No Python, a digitação dinâmica oferece uma tremenda liberdade de ação. E onde há maior liberdade de ação - é mais provável que você atire no próprio pé.


Vale esclarecer que a digitação em Python é estrita e a adição de um número a uma string não funciona:


 1 + '1' >>> TypeError: unsupported operand type(s) for +: 'int' and 'str' 

O Python também verifica a assinatura da função quando é chamada e lança uma exceção se a assinatura da chamada não for verdadeira:


 def sum(x, y): return x + y sum(10, 20, 30) >>> TypeError: sum() takes 2 positional arguments but 3 were given 

Porém, ao carregar um script, o Python não informará que a função espera um número e não uma string que você passa para ele. E você só aprende sobre isso em tempo de execução:


 def sum(x, y): return x + y sum(10, '10') >>> TypeError: can only concatenate str (not "int") to str 

Quanto mais forte o desafio para o programador, especialmente ao escrever grandes projetos . O Python moderno respondeu a esse desafio com um mecanismo de anotação e uma biblioteca de tipos, e a comunidade desenvolveu programas que executam a verificação de tipo estático . Como resultado, o programador aprende sobre esses erros antes de executar o programa:


 # main.py: def sum(x: int, y: int) -> int: return x + y sum(10, '10') $ mypy main.py tmp.py:5: error: Argument 2 to "sum" has incompatible type "str"; expected "int" 

O Python não atribui nenhuma importância às anotações, embora as armazene no atributo __annotations__ . A única condição é que as anotações sejam valores válidos em termos de idioma. Desde a sua aparição na versão 3.0 (que foi há mais de dez anos!), Foram os esforços da comunidade que começaram a usar anotações para marcação digitada de variáveis ​​e argumentos.


Outro exemplo, mais complicado.
 #      , :   :) from typing import TypeVar, Iterable Num = TypeVar('Num', int, float) def sum(items: Iterable[Num]) -> Num: accum = 0 for item in items: accum += item return accum sum([1, 2, 3]) >>> 6 

Dica 2: Na prática, a digitação mais dinâmica causa problemas ao ler e depurar código. Especialmente se esse código foi escrito sem anotações e você precisa gastar muito tempo descobrindo os tipos de variáveis. Você não precisa indicar e documentar os tipos de tudo, mas o tempo gasto em uma descrição detalhada das interfaces públicas e nas seções mais críticas do código será recompensado em cem vezes!


Quack! Digitação de pato


Às vezes, os entusiastas de Python fingem ser misteriosos e falam sobre "digitação de pato".
Digitação de pato é o uso do teste de pato na programação:


Se um objeto se agita como um pato, voa como um pato e anda como um pato, provavelmente é um pato.

Considere um exemplo:


 class RpgCharacter: def __init__(self, weapon) self.weapon = weapon def battle(self): self.weapon.attack() 

Aqui está a injeção de dependência clássica. A classe RpgCharacter recebe o objeto de weapon no construtor e, mais tarde, no método battle() , chama weapon.attack() . Mas o RpgCharacter não depende da implementação específica da weapon . Pode ser uma espada, um BFG 9000 ou uma baleia com um vaso de flores, pronta para pousar na cabeça do inimigo a qualquer momento. É importante que o objeto tenha um método attack() , pois o Python não está interessado em tudo o mais.



A rigor, a digitação de patos não é única. Está presente em todas as linguagens dinâmicas (familiares para mim) que implementam OOP.


Este é outro exemplo de como programar cuidadosamente no mundo da digitação dinâmica. Método mal nomeado? Nomeada uma variável de forma ambígua? Seu colega, ou você mesmo, depois de cerca de meio ano, ficará feliz em usar esse código :)


O que aconteceria se usarmos Java condicional?
 interface IWeapon { void attack(); } public class Sword implements IWeapon { public void attack() { //... } } public class RpgCharacter { IWeapon weapon; public RpgCharacter(IWeapon weapon) { this.weapon = weapon; } public void battle() { weapon.attack(); } } 

E haveria uma digitação estática clássica, com verificação de tipo no estágio de compilação. Preço - a incapacidade de usar um objeto que possui um método attack() , mas não implementa explicitamente a interface IWeapon .


Dica 3 : Se desejar, você pode descrever a interface criando sua própria classe abstrata com métodos e propriedades . Melhor ainda, dedique algum tempo testando e escrevendo a documentação para você e seus usuários de código.


Abordagem processual e __ métodos especiais __ ()


Python é uma linguagem orientada a object e a classe de object está na raiz da hierarquia de herança:


 isinstance('abc', object) >>> True isinstance(10, object) >>> True 

Mas onde obj.ToString() usado em Java e C #, haverá uma chamada para str(obj) em Python. Ou, por exemplo, em vez de myList.length , o Python terá len(my_list) . O criador da linguagem, Guido van Rossum, explicou o seguinte:


Quando leio o código que diz len(x) , sei que o tamanho de algo está sendo solicitado. Isso imediatamente me diz que o resultado será um número inteiro e o argumento é algum tipo de contêiner. Por outro lado, ao ler x.len() , preciso saber que x é algum tipo de contêiner que implementa uma interface específica ou herda de uma classe que possui o método len() . [Fonte] .

No entanto, dentro de si, as funções len() , str() e algumas outras chamarão certos métodos do objeto:


 class User: def __init__(self, name, last_name): self.name = name self.last_name = last_name def __str__(self): return f"Honourable {self.name} {self.last_name}" u = User('Alex', 'Black') label = str(u) print(label) >>> Honourable Alex Black 

Métodos especiais também são usados ​​por operadores de linguagem, matemáticos e booleanos, bem como for ... in ... operadores de loop, with operador de contexto, operador de índice [] , etc.
Por exemplo, um protocolo iterador consiste em dois métodos: __iter__() e __next__() :


 #  Iterable, IEnumerable, std::iterator  .. class InfinitePositiveIntegers: def __init__(self): self.counter = 0 def __iter__(self): """      .    iter(). """ return self def __next__(self): """  .    next(). """ self.counter += 1 return self.counter for i in InfinitePositiveIntegers(): print(i) >>> 1 >>> 2 >>> ... #  ,  Ctrl + C 

Bem, digamos métodos especiais. Mas por que eles parecem tão distorcidos? Guido explicou isso pelo fato de terem os nomes usuais sem sublinhar, os próprios programadores não os redefiniriam, mais cedo ou mais tarde. I.e. ____() é um tipo de proteção contra o tolo. Como o tempo mostrou - a proteção é eficaz :)


Dica 4: observe atentamente as funções internas e os métodos de objetos especiais . Eles são parte integrante da linguagem, sem a qual é impossível falar completamente.


Onde está o encapsulamento? Onde está o meu privado ?! Onde está o meu conto de fadas? !!


Python não possui modificadores de acesso para atributos de classe. O interior dos objetos está aberto para acesso sem restrições. No entanto, existe uma convenção segundo a qual os atributos com o prefixo _ são considerados privados, por exemplo:


 import os class MyFile: #    _os_handle = None def __init__(self, path: str): self._open(path) #    def _open(self, path): # os.open() - **    . #      open(). #   os.open()    . self._os_handle = os.open(path, os.O_RDWR | os.O_CREAT) #      def close(self): if self._os_handle is not None: os.close(self._os_handle) f = MyFile('/tmp/file.txt') print(f._os_handle) #    ""    ! f.close() 

Porque


Não há nada particular em Python. Nem a classe nem sua instância irão esconder de você o que está dentro (graças ao qual a introspecção mais profunda é possível). Python confia em você. Ele meio que diz: "Amigo, se você quiser remexer nos cantos escuros - não há problema. Acredito que existem boas razões para isso e espero que você não quebre nada. É claro.

No final, somos todos adultos aqui.

- Karl Fast [fonte] .

Mas como evitar colisões de nomes durante a herança?

O Python possui um mecanismo especial para manipular o nome dos atributos começando com sublinhado duplo e não terminando com sublinhado duplo ( __my_attr )! Isso é feito para evitar colisões de nomes durante a herança. Para chamar os métodos de classe fora do corpo, o Python adiciona o prefixo ___ . Mas para acesso interno, nada muda:


 class C: def __init__(self): self.__x = 10 def get_x(self): return self.__x c = C() c.__x >>> 'C' object has no attribute '__x' print(c.get_x()) >>> 10 print(c._C__x) >>> 10 

Vamos olhar para uma aplicação prática. Por exemplo, para a classe File , que lê arquivos do sistema de arquivos local, queremos adicionar recursos de cache. Nosso colega conseguiu escrever uma aula de mixin para esses fins. Mas, para isolar métodos e atributos de possíveis conflitos, um colega adicionou o prefixo __ a seus nomes:


 class BaseFile: def __init__(self, path): self.path = path class LocalMixin: def read_from_local(self): with open(self.path) as f: return f.read() class CachedMixin: class CacheMissError(Exception): pass def __init__(self): # Tepe,         #   __cache,   __from_cache(), # ,     ! self.__cache = {} def __from_cache(self): return self.__cache[self.path] def read_from_cache(self): try: return self.__from_cache() except KeyError as e: raise self.CacheMissError() from e def store_to_cache(self, data): self.__cache[self.path] = data class File(CachedMixin, LocalMixin, BaseFile): def __init__(self, path): CachedMixin.__init__(self) BaseFile.__init__(self, path) def read(self): try: return self.read_from_cache() except CachedMixin.CacheMissError: data = self.read_from_local() self.store_to_cache(data) return data 

Se você estiver interessado em examinar a implementação desse mecanismo no CPython, por favor, em Python / compile.c


Finalmente, devido à presença de propriedades na linguagem, não faz sentido escrever getters e setters no estilo Java: getX(), setX() . Por exemplo, na classe originalmente escrita Coordinates ,


 class Coordinates: def __init__(self, x, y): self.x = x self.y = y c = Coordinates(10, 10) print(cx, cy) >>> (10, 10) 

Eu precisava controlar o acesso ao atributo x . A abordagem correta seria substituí-la por property , mantendo assim um contrato com o mundo exterior.


 class Coordinates: _x = 0 def __init__(self, x, y): self.x = x self.y = y @property def x(self): return self._x @x.setter def x(self, val): if val > 10: self._x = val else: raise ValueError('x should be greater than 10') c = Coordinates(20, 10) cx = 5 >>> ValueError: x should be greater than 10 

Dica 5: Assim como em Python, o conceito de campos particulares e métodos de classe é baseado em uma convenção estabelecida. Não se ofenda com os autores das bibliotecas se "tudo parou de funcionar" pelo motivo de você ter usado ativamente os campos particulares de suas classes. No final, somos todos adultos aqui :) .


Um pouco sobre exceções


A cultura Python tem uma abordagem única para exceções. Além da interceptação e processamento usuais em C ++ / Java, você encontrará o uso de exceções no contexto


"Mais fácil pedir perdão do que permissão - EAFP."

Parafraseando - não escreva muito if , if , na maioria dos casos, a execução for neste ramo. Em vez disso, envolva a lógica em try..except .


Exemplo: imagine um manipulador de solicitação POST que crie um usuário em um banco de dados condicional. Na entrada da função, há um dicionário (dicionário) do tipo de valor-chave:


 def create_user_handler(data: Dict[str, str]): try: database.user.persist( username=data['username'], password=data['password'] ) except KeyError: print('There was a missing field in data passed for user creation') 

Não poluimos o código com verificações "se o username ou a password estão nos data ". Esperamos que eles provavelmente estejam lá. Não pedimos “permissão” para usar esses campos, mas “pedimos desculpas” quando o próximo kulhacker postar um formulário com dados ausentes.


Só não leve ao ponto do absurdo!

Por exemplo, você gostaria de verificar se o sobrenome do usuário está presente nos dados e se não o define como um valor vazio. if aqui fosse muito mais apropriado:


 def create_user_handler(data): if 'last_name' not in data: data['last_name'] = '' try: database.user.persist( username=data['username'], password=data['password'], last_name=data['last_name'] ) except KeyError: print('There was a missing field in data passed for user creation') 

Os erros nunca devem passar silenciosamente. - não ignore as exceções! O Python moderno tem um raise from maravilhoso raise from construção que permite manter o contexto da cadeia de exceções. Por exemplo:


 class MyProductError(Exception): def __init__(self): super().__init__('There has been a terrible product error') def calculate(x): try: return 10 / x except ZeroDivisionError as e: raise MyProductError() from e 

Sem raise from e cadeia de exceção é interrompida em MyProductError e não podemos descobrir qual foi exatamente a causa desse erro. Com raise from X , o motivo (ou seja, X ) da exceção lançada é armazenado no atributo __cause__ :


 try: calculate(0) except MyProductError as e: print(e.__cause__) >>> division by zero 

Mas há uma pequena nuance no caso da iteração: StopIteration

No caso de uma iteração, o lançamento de uma exceção StopIteration é a maneira oficial de sinalizar que o iterador está completo.


 class PositiveIntegers: def __init__(self, limit): self.counter = 0 self.limit = limit def __iter__(self): return self def __next__(self): self.counter += 1 if self.counter == self.limit: #  hasNext()  moveNext(), #  ,   raise StopIteration() return self.counter for i in PositiveIntegers(5): print(i) > 1 > 2 > 3 > 4 

Dica 6: pagamos pelo tratamento de exceções somente em situações excepcionais. Não os negligencie!


Deve haver um - e de preferência apenas um - a maneira anterior de fazer isso.


switch ou correspondência de padrões? - use if e dicionários. do- ? - para isso há um while e for . goto ? Eu acho que você mesmo adivinhou. O mesmo se aplica a algumas técnicas e padrões de design que parecem ter sido concedidos em outros idiomas. O mais surpreendente é que não há restrições técnicas à sua implementação, é apenas "não aceitamos".


Por exemplo, no Python, você não vê frequentemente o padrão "Construtor". Em vez disso, ele usa a capacidade de transmitir e solicitar explicitamente argumentos de nome para a função. Em vez disso


 human = HumanBuilder.withName("Alex").withLastName("Black").ofAge(20).withHobbies(['tennis', 'programming']).build() 

será


 human = Human( name="Alex" last_name="Black" age=20 hobbies=['tennis', 'programming'] ) 

A biblioteca padrão não usa cadeias de métodos para trabalhar com coleções . Lembro-me de como um colega que veio do mundo de Kotlin me mostrou o código do seguinte sentido (extraído da documentação oficial do Kotlin):


 val shortGreetings = people .filter { it.name.length < 10 } .map { "Hello, ${it.name}!" } 

No Python, map() , filter() e muitos outros são funções, não métodos de coleta. Reescrevendo este código um por um, obtemos:


 short_greetings = map(lambda h: f"Hello, {h.name}", filter(lambda h: len(h.name) < 10, people)) 

Na minha opinião, parece horrível. Portanto, para pacotes grandes como .takewhile().filter().map().reduce() , é melhor usar o chamado inclusão (compreensões) ou bons ciclos antigos. A propósito, o mesmo exemplo no Kotlin é dado na forma da compreensão da lista correspondente. E no Python é assim:


 short_greetings = [ f"Hello {h.name}" for h in people if len(h.name) < 10 ] 

Para quem sente falta das correntes

Existem bibliotecas como Pipe ou py_linq !


As cadeias de métodos são usadas onde são mais eficientes que as ferramentas padrão. Por exemplo, na estrutura da web do Django, cadeias são usadas para criar um objeto de consulta ao banco de dados:


 query = User.objects \ .filter(last_visited__gte='2019-05-01') \ .order_by('username') \ .values('username', 'last_visited') \ [:5] 

Dica 7: Antes de fazer algo muito familiar com a experiência passada, mas não familiar no Python, pergunte-se que decisão um pythonist experiente tomaria?


Python lento


Sim


Sim, quando se trata de velocidade de execução em comparação com linguagens estaticamente tipadas e compiladas.


Mas você parece querer uma resposta detalhada?


A implementação de referência do Python (CPython) está longe de ser a sua implementação mais eficaz. Uma das razões importantes é o desejo dos desenvolvedores de não complicá-lo. E a lógica é compreensível - o código não muito obscuro significa menos erros, uma melhor oportunidade para fazer alterações e, no final, mais pessoas que desejam ler, entender e complementar esse código.


Jake VanderPlas, em seu blog, analisa o que acontece no CPython ao adicionar duas variáveis ​​que contêm valores inteiros:


 a = 1 b = 2 c = a + b 

Mesmo se não nos aprofundarmos na selva do CPython, podemos dizer que, para armazenar as variáveis a , b , o intérprete precisará criar três objetos na pilha, nos quais os valores de tipo e (indicadores para) serão armazenados; binary_add<int, int>(a->val, b->val) novamente o tipo e os valores durante a operação de adição para chamar algo como binary_add<int, int>(a->val, b->val) ; escreva o resultado em c .
Isso é terrivelmente ineficiente em comparação com um programa C semelhante.


Outro problema com o CPython é o chamado Bloqueio Global de Intérpretes (GIL). Esse mecanismo, essencialmente um valor booleano delimitado por um mutex, é usado para sincronizar a execução do bytecode. O GIL simplifica o desenvolvimento de código em execução em um ambiente multiencadeado: o CPython não precisa pensar em sincronizar o acesso a variáveis ​​ou deadlocks. Você precisa pagar por isso, pois apenas um thread obtém acesso e executa o bytecode em um determinado momento:



UPD: Mas isso não significa que o programa em Python funcione magicamente em um ambiente multiencadeado! O código no Python não é transferido para o bytecode um por um e não há garantias sobre a compatibilidade do bytecode entre as versões! Portanto, você ainda precisa sincronizar os threads no código. Felizmente, aqui o Python possui um rico conjunto de ferramentas, por exemplo, permitindo que você alterne entre um modelo de execução multiencadeado e multiprocesso.


Se você está curioso sobre quais esforços estão sendo feitos para erradicar o GIL

?


  1. . ( CFFI ) . API (extensions) C/C++. , Rust, Go Kotlin Native !
  2. , :

8: , . , IO (, , ) , , , , :)



? Linux MacOS, 95% . , 3., 2.7. Windows . : Docker, Windows Subsystem for Linux, Cygwin, , .


9: . , — - .


"Hello world" ? Ótimo! machine learning- - Python Package Index (PyPI).


(packages), .. (virtual environments). , . - . pip . pip . , pipenv poetry — npm, bundler, cargo ..


0xA: pip virtualenv . — , , . , — sys.path — , .


?


? . :


Dive into python...

, . , , :)


, !

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


All Articles