├── .gitattributes ├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── LICENSE ├── Makefile ├── README.md ├── conftest.py └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-documentation=false 2 | *.md linguist-language=Python 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # VSCODE 128 | .vscode 129 | .idea 130 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8.3" 4 | # command to install dependencies 5 | install: 6 | - make deps 7 | # command to run tests 8 | script: 9 | - make tests 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Rigel Di Scala 2 | Zachary Anglin 3 | AirbusDriver 4 | Micheal 5 | Erik OShaughnessy 6 | Mukhammad Karimov 7 | sitnarf 8 | Miguel Gonzalez 9 | Anvar 10 | Martin Pavlásek 11 | Shahrukh Khan 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan McDermott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps clean tests 2 | 3 | ENV=.env 4 | PYTHON=python3.8 5 | PYTHON_VERSION=$(shell ${PYTHON} -V | cut -d " " -f 2 | cut -c1-3) 6 | SITE_PACKAGES=${ENV}/lib/python${PYTHON_VERSION}/site-packages 7 | IN_ENV=source ${ENV}/bin/activate; 8 | 9 | default: tests 10 | 11 | ${ENV}: 12 | @echo "Creating Python environment..." >&2 13 | @${PYTHON} -m venv ${ENV} 14 | @echo "Updating pip..." >&2 15 | @${IN_ENV} pip install -U pip 16 | 17 | ${SITE_PACKAGES}/pytest.py: 18 | @${IN_ENV} pip install -r requirements.txt 19 | 20 | deps: ${SITE_PACKAGES}/pytest.py 21 | 22 | tests: ${ENV} ${SITE_PACKAGES}/pytest.py 23 | @${IN_ENV} pytest 24 | 25 | clean: 26 | @rm -rf ${ENV} .env .pytest_cache 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clean-code-python 2 | 3 | [![Build Status](https://travis-ci.com/zedr/clean-code-python.svg?branch=master)](https://travis-ci.com/zedr/clean-code-python) 4 | [![](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/download/releases/3.8.3/) 5 | 6 | ## Índice 7 | 1. [Introdução](#introdução) 8 | 2. [Variáveis](#variáveis) 9 | 3. [Funções](#funções) 10 | 4. [Objects and Data Structures](#objetos-e-estruturas-de-dados) 11 | 5. [Classes](#classes) 12 | 1. [S: Princípio da Responsabilidade Única (SRP)](#princípio-da-responsabilidade-Única-srp) 13 | 2. [O: Princípio do Aberto/Fechado (OCP)](#princípio-do-abertofechado-ocp) 14 | 3. [L: Princípio de Substituição de Liskov (LSP)](#princípio-de-substituição-de-liskov-lsp) 15 | 4. [I: Princípio da Segregação de Interface (ISP)](#princípio-da-segregação-de-interface-isp) 16 | 5. [D: Princípio da Inversão de Dependência (DIP)](#princípio-da-inversão-de-dependência-dip) 17 | 6. [Não se repita (DRY)](#não-se-repita-dry) 18 | 19 | ## Introdução 20 | 21 | Principios da engenharia de software, do livro de Robert C. Martin 22 | [*Código Limpo*](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882), 23 | adaptados para Python. Isto não é um style guide. É um guia para desenvolver software legível, reutilizavel e refatorável em Python. 24 | 25 | Nem todos principios contidos aqui tem de ser seguidos estritamente, e muito menos irão ser universalmente aprovados. Estes são apenas guias e nada mais, mas que foram codificados durante muito anos por experiências coletivas dos autores de *Código Limpo*. 26 | 27 | Inspriado em [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript) 28 | 29 | Versão Python3.7+ 30 | 31 | ## **Variáveis** 32 | ### Use nomes significantes e pronunciáveis em suas variáveis 33 | 34 | **Ruim:** 35 | ```python 36 | import datetime 37 | 38 | 39 | ymdstr = datetime.date.today().strftime("%y-%m-%d") 40 | ``` 41 | 42 | **Bom**: 43 | ```python 44 | import datetime 45 | 46 | 47 | current_date: str = datetime.date.today().strftime("%y-%m-%d") 48 | ``` 49 | **[⬆ back to top](#índice)** 50 | 51 | ### Use o mesmo vocabulário para o mesmo tipo de variável 52 | 53 | **Ruim:** 54 | Usamos três nomes diferentes para a mesma entidade: 55 | ```python 56 | def get_user_info(): pass 57 | def get_client_data(): pass 58 | def get_customer_record(): pass 59 | ``` 60 | 61 | **Bom**: 62 | Se a entidade for a mesma, você deve ser consistente ao se referir a ela em suas funções: 63 | ```python 64 | def get_user_info(): pass 65 | def get_user_data(): pass 66 | def get_user_record(): pass 67 | ``` 68 | 69 | **Melhor ainda**: 70 | Python é (também) uma linguagem de programação orientada a objetos. Se fizer sentido, empacote as funções junto com a implementação concreta da entidade em seu código, como atributos de instância, métodos ou métodos de propriedade: 71 | 72 | ```python 73 | from typing import Union, Dict, Text 74 | 75 | 76 | class Record: 77 | pass 78 | 79 | 80 | class User: 81 | info : str 82 | 83 | @property 84 | def data(self) -> Dict[Text, Text]: 85 | return {} 86 | 87 | def get_record(self) -> Union[Record, None]: 88 | return Record() 89 | ``` 90 | 91 | **[⬆ back to top](#índice)** 92 | 93 | ### Use nomes fáceis de pesquisar 94 | Nós vamos ler mais código do que escrever, por isso é importante que o código que escrevemos seja legível e fácil de achar. Ao *não* nomear variáveis, prejudicamos nossos leitores. 95 | Torne seus nomes fáceis de procurar. 96 | 97 | **Ruim:** 98 | ```python 99 | import time 100 | 101 | 102 | # Para que é o número 86400? 103 | time.sleep(86400) 104 | ``` 105 | 106 | **Bom**: 107 | ```python 108 | import time 109 | 110 | 111 | # Declare-os no namespace global do módulo. 112 | SECONDS_IN_A_DAY = 60 * 60 * 24 113 | time.sleep(SECONDS_IN_A_DAY) 114 | ``` 115 | **[⬆ back to top](#índice)** 116 | 117 | ### Use variáveis explicativas 118 | **Ruim:** 119 | ```python 120 | import re 121 | 122 | 123 | address = "One Infinite Loop, Cupertino 95014" 124 | city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$" 125 | 126 | matches = re.match(city_zip_code_regex, address) 127 | if matches: 128 | print(f"{matches[1]}: {matches[2]}") 129 | ``` 130 | 131 | **Nada mal**: É melhor, mas ainda dependemos muito do regex. 132 | 133 | ```python 134 | import re 135 | 136 | 137 | address = "One Infinite Loop, Cupertino 95014" 138 | city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$" 139 | matches = re.match(city_zip_code_regex, address) 140 | 141 | if matches: 142 | city, zip_code = matches.groups() 143 | print(f"{city}: {zip_code}") 144 | ``` 145 | 146 | **Bom**: Diminua a dependência de regex nomeando as variáveis em subgrupo 147 | ```python 148 | import re 149 | 150 | 151 | address = "One Infinite Loop, Cupertino 95014" 152 | city_zip_code_regex = r"^[^,\\]+[,\\\s]+(?P.+?)\s*(?P\d{5})?$" 153 | 154 | matches = re.match(city_zip_code_regex, address) 155 | if matches: 156 | print(f"{matches['city']}, {matches['zip_code']}") 157 | ``` 158 | **[⬆ back to top](#índice)** 159 | 160 | ### Evite mapear mentalmente 161 | Não force o leitor do seu código a traduzir o que a variável significa. 162 | Explicito é melhor que implito. 163 | 164 | **Ruim:** 165 | ```python 166 | seq = ("Austin", "New York", "San Francisco") 167 | 168 | for item in seq: 169 | #do_stuff() 170 | #do_some_other_stuff() 171 | 172 | # Espere, `item` de novo? 173 | print(item) 174 | ``` 175 | 176 | **Bom**: 177 | ```python 178 | locations = ("Austin", "New York", "San Francisco") 179 | 180 | for location in locations: 181 | #do_stuff() 182 | #do_some_other_stuff() 183 | # ... 184 | print(location) 185 | ``` 186 | **[⬆ back to top](#índice)** 187 | 188 | 189 | ### Não adicione contextos desnecessários 190 | 191 | Se o nome da sua classe/objeto expressa algo, não repita isso no nome da variável. 192 | 193 | **Ruim:** 194 | 195 | ```python 196 | class Car: 197 | car_make: str 198 | car_model: str 199 | car_color: str 200 | ``` 201 | 202 | **Bom**: 203 | 204 | ```python 205 | class Car: 206 | make: str 207 | model: str 208 | color: str 209 | ``` 210 | 211 | **[⬆ back to top](#índice)** 212 | 213 | ### Use argumentos padrões ao invés de encadear condicionais 214 | 215 | **Muito ruim** 216 | 217 | Porque escrever: 218 | 219 | ```python 220 | import hashlib 221 | 222 | 223 | def create_micro_brewery(name): 224 | name = "Hipster Brew Co." if name is None else name 225 | slug = hashlib.sha1(name.encode()).hexdigest() 226 | # etc. 227 | ``` 228 | 229 | ... quando você pode especificar um argumento padrão em vez disso? Isso também deixa claro que 230 | você está esperando uma string como argumento. 231 | 232 | **Bom**: 233 | 234 | ```python 235 | from typing import Text 236 | import hashlib 237 | 238 | 239 | def create_micro_brewery(name: Text = "Hipster Brew Co."): 240 | slug = hashlib.sha1(name.encode()).hexdigest() 241 | # etc. 242 | ``` 243 | 244 | **[⬆ back to top](#índice)** 245 | ## **Funções** 246 | ### Argumentos de funções (2 ou menos, idealmente) 247 | Limitar a quantidade de parametros de uma função é incrivelmente importantante porque isso torna sua função fácil de testar. Ter mais de três de leva em uma explosão onde você tem que testar vários casos diferentes, com argumentos separados. 248 | 249 | Um ou dois argumentos é o caso ideal, e três deve ser evitado se possível. Algo além disso deve ser deixado de lado. Usualmente, se você tem mais de dois argumentos, suas funções estão tentando fazer coisas demais. Nos casos que não estão, na maior parte do tempo um objeto irá ser o suficiente como argumento. 250 | 251 | **Ruim:** 252 | ```python 253 | def create_menu(title, body, button_text, cancellable): 254 | pass 255 | ``` 256 | 257 | **Java-esque**: 258 | ```python 259 | class Menu: 260 | def __init__(self, config: dict): 261 | self.title = config["title"] 262 | self.body = config["body"] 263 | # ... 264 | 265 | menu = Menu( 266 | { 267 | "title": "My Menu", 268 | "body": "Something about my menu", 269 | "button_text": "OK", 270 | "cancellable": False 271 | } 272 | ) 273 | ``` 274 | 275 | **Muito bom** 276 | ```python 277 | from typing import Text 278 | 279 | 280 | class MenuConfig: 281 | """A configuration for the Menu. 282 | 283 | Attributes: 284 | title: The title of the Menu. 285 | body: The body of the Menu. 286 | button_text: The text for the button label. 287 | cancellable: Can it be cancelled? 288 | """ 289 | title: Text 290 | body: Text 291 | button_text: Text 292 | cancellable: bool = False 293 | 294 | 295 | def create_menu(config: MenuConfig) -> None: 296 | title = config.title 297 | body = config.body 298 | # ... 299 | 300 | 301 | config = MenuConfig() 302 | config.title = "My delicious menu" 303 | config.body = "A description of the various items on the menu" 304 | config.button_text = "Order now!" 305 | # O atributo de instância substitui o atributo de classe padrão. 306 | config.cancellable = True 307 | 308 | create_menu(config) 309 | ``` 310 | 311 | **Chique** 312 | ```python 313 | from typing import NamedTuple 314 | 315 | 316 | class MenuConfig(NamedTuple): 317 | """A configuration for the Menu. 318 | 319 | Attributes: 320 | title: The title of the Menu. 321 | body: The body of the Menu. 322 | button_text: The text for the button label. 323 | cancellable: Can it be cancelled? 324 | """ 325 | title: str 326 | body: str 327 | button_text: str 328 | cancellable: bool = False 329 | 330 | 331 | def create_menu(config: MenuConfig): 332 | title, body, button_text, cancellable = config 333 | # ... 334 | 335 | 336 | create_menu( 337 | MenuConfig( 338 | title="My delicious menu", 339 | body="A description of the various items on the menu", 340 | button_text="Order now!" 341 | ) 342 | ) 343 | ``` 344 | 345 | **Ainda mais chique** 346 | ```python 347 | from typing import Text 348 | from dataclasses import astuple, dataclass 349 | 350 | 351 | @dataclass 352 | class MenuConfig: 353 | """A configuration for the Menu. 354 | 355 | Attributes: 356 | title: The title of the Menu. 357 | body: The body of the Menu. 358 | button_text: The text for the button label. 359 | cancellable: Can it be cancelled? 360 | """ 361 | title: Text 362 | body: Text 363 | button_text: Text 364 | cancellable: bool = False 365 | 366 | def create_menu(config: MenuConfig): 367 | title, body, button_text, cancellable = astuple(config) 368 | # ... 369 | 370 | 371 | create_menu( 372 | MenuConfig( 373 | title="My delicious menu", 374 | body="A description of the various items on the menu", 375 | button_text="Order now!" 376 | ) 377 | ) 378 | ``` 379 | 380 | **Ainda mais chique, versões Python3.8+** 381 | ```python 382 | from typing import TypedDict, Text 383 | 384 | 385 | class MenuConfig(TypedDict): 386 | """A configuration for the Menu. 387 | 388 | Attributes: 389 | title: The title of the Menu. 390 | body: The body of the Menu. 391 | button_text: The text for the button label. 392 | cancellable: Can it be cancelled? 393 | """ 394 | title: Text 395 | body: Text 396 | button_text: Text 397 | cancellable: bool 398 | 399 | 400 | def create_menu(config: MenuConfig): 401 | title = config["title"] 402 | # ... 403 | 404 | 405 | create_menu( 406 | # Você precisa informar todos os parâmetros 407 | MenuConfig( 408 | title="My delicious menu", 409 | body="A description of the various items on the menu", 410 | button_text="Order now!", 411 | cancellable=True 412 | ) 413 | ) 414 | ``` 415 | **[⬆ back to top](#índice)** 416 | 417 | ### Funções devem fazer somente uma coisa 418 | Esta é, de longe, a regra mais importante da engenharia de software. Quando as funções fazem mais de uma coisa, elas são mais difíceis de compor, testar e pensar sobre. Quando você consegue isolar a função para apenas uma ação, elas podem ser refatoradas sem muita dificuldade e seu código será fácilmente lido. Se você não tirar mais nada deste guia além disso, você estará à frente de muitos programadores. 419 | 420 | **Ruim:** 421 | ```python 422 | from typing import List 423 | 424 | 425 | class Client: 426 | active: bool 427 | 428 | 429 | def email(client: Client) -> None: 430 | pass 431 | 432 | 433 | def email_clients(clients: List[Client]) -> None: 434 | """Filter active clients and send them an email. 435 | """ 436 | for client in clients: 437 | if client.active: 438 | email(client) 439 | ``` 440 | 441 | **Bom**: 442 | ```python 443 | from typing import List 444 | 445 | 446 | class Client: 447 | active: bool 448 | 449 | 450 | def email(client: Client) -> None: 451 | pass 452 | 453 | 454 | def get_active_clients(clients: List[Client]) -> List[Client]: 455 | """Filter active clients. 456 | """ 457 | return [client for client in clients if client.active] 458 | 459 | 460 | def email_clients(clients: List[Client]) -> None: 461 | """Send an email to a given list of clients. 462 | """ 463 | for client in get_active_clients(clients): 464 | email(client) 465 | ``` 466 | 467 | Você vê uma oportunidade para usar geradores agora? 468 | 469 | **Melhor ainda** 470 | ```python 471 | from typing import Generator, Iterator 472 | 473 | 474 | class Client: 475 | active: bool 476 | 477 | 478 | def email(client: Client): 479 | pass 480 | 481 | 482 | def active_clients(clients: Iterator[Client]) -> Generator[Client, None, None]: 483 | """Only active clients""" 484 | return (client for client in clients if client.active) 485 | 486 | 487 | def email_client(clients: Iterator[Client]) -> None: 488 | """Send an email to a given list of clients. 489 | """ 490 | for client in active_clients(clients): 491 | email(client) 492 | ``` 493 | 494 | 495 | **[⬆ back to top](#índice)** 496 | 497 | ### Nomes das funções devem dizer o que elas fazem 498 | 499 | **Ruim:** 500 | 501 | ```python 502 | class Email: 503 | def handle(self) -> None: 504 | pass 505 | 506 | message = Email() 507 | # O que isso quer dizer? 508 | message.handle() 509 | ``` 510 | 511 | **Bom:** 512 | 513 | ```python 514 | class Email: 515 | def send(self) -> None: 516 | """Send this message""" 517 | 518 | message = Email() 519 | message.send() 520 | ``` 521 | 522 | **[⬆ back to top](#índice)** 523 | 524 | ### Funções devem estar em apenas um nível de abstração 525 | 526 | Quando você tem mais de um nível de abstração possívelmente sua função está fazendo coisa demais. Dividir suas funções desencadeia em código reusável e fácil de testar. 527 | 528 | **Ruim:** 529 | 530 | ```python 531 | # type: ignore 532 | 533 | def parse_better_js_alternative(code: str) -> None: 534 | regexes = [ 535 | # ... 536 | ] 537 | 538 | statements = code.split('\n') 539 | tokens = [] 540 | for regex in regexes: 541 | for statement in statements: 542 | pass 543 | 544 | ast = [] 545 | for token in tokens: 546 | pass 547 | 548 | for node in ast: 549 | pass 550 | ``` 551 | 552 | **Bom:** 553 | 554 | ```python 555 | from typing import Tuple, List, Text, Dict 556 | 557 | 558 | REGEXES: Tuple = ( 559 | # ... 560 | ) 561 | 562 | 563 | def parse_better_js_alternative(code: Text) -> None: 564 | tokens: List = tokenize(code) 565 | syntax_tree: List = parse(tokens) 566 | 567 | for node in syntax_tree: 568 | pass 569 | 570 | 571 | def tokenize(code: Text) -> List: 572 | statements = code.split() 573 | tokens: List[Dict] = [] 574 | for regex in REGEXES: 575 | for statement in statements: 576 | pass 577 | 578 | return tokens 579 | 580 | 581 | def parse(tokens: List) -> List: 582 | syntax_tree: List[Dict] = [] 583 | for token in tokens: 584 | pass 585 | 586 | return syntax_tree 587 | ``` 588 | 589 | **[⬆ back to top](#índice)** 590 | 591 | ### Não use sinalizadores como parâmetros de função 592 | 593 | Os sinalizadores informam ao usuário que esta função faz mais de uma coisa. Funções 594 | deve fazer uma coisa. Divida suas funções se elas estiverem seguindo um código diferente 595 | caminhos baseados em verdadeiro ou falso. 596 | 597 | **Ruim:** 598 | 599 | ```python 600 | from typing import Text 601 | from tempfile import gettempdir 602 | from pathlib import Path 603 | 604 | 605 | def create_file(name: Text, temp: bool) -> None: 606 | if temp: 607 | (Path(gettempdir()) / name).touch() 608 | else: 609 | Path(name).touch() 610 | ``` 611 | 612 | **Bom:** 613 | 614 | ```python 615 | from typing import Text 616 | from tempfile import gettempdir 617 | from pathlib import Path 618 | 619 | 620 | def create_file(name: Text) -> None: 621 | Path(name).touch() 622 | 623 | 624 | def create_temp_file(name: Text) -> None: 625 | (Path(gettempdir()) / name).touch() 626 | ``` 627 | 628 | **[⬆ back to top](#índice)** 629 | 630 | ### Evite efeitos colaterais 631 | 632 | Uma função produz um efeito colateral se fizer qualquer coisa além de assumir um valor ao invés de retornar outro valor ou valores. Por exemplo, um efeito colateral pode ser a escrita 633 | a um arquivo, modificando alguma variável global ou transferindo acidentalmente todo o seu dinheiro 634 | para um estranho. 635 | 636 | No entanto, você precisa ter efeitos colaterais em um programa de vez em quando - por exemplo, como 637 | no exemplo anterior, você pode precisar gravar em um arquivo. Nestes casos, você 638 | deve centralizar e indicar onde você está incorporando efeitos colaterais. Não tem 639 | várias funções e classes que gravam em um arquivo específico - em vez disso, têm um 640 | (e apenas um) serviço que o faz. 641 | 642 | O ponto principal é evitar armadilhas comuns, como o compartilhamento de estado entre objetos 643 | sem qualquer estrutura, usando tipos de dados mutáveis ​​que podem ser gravados por qualquer coisa ou usando uma instância de uma classe, e não centralizando onde ocorrem seus efeitos colaterais. 644 | Se você puder fazer isso, ficará mais feliz do que a grande maioria dos outros programadores. 645 | 646 | **Ruim:** 647 | 648 | ```python 649 | # type: ignore 650 | 651 | # Este é um nome de nível de módulo.. 652 | # É uma boa prática defini-los como valores imutáveis, como uma string. 653 | # No entanto... 654 | fullname = "Ryan McDermott" 655 | 656 | def split_into_first_and_last_name() -> None: 657 | # O uso da palavra-chave global aqui está mudando o significado da 658 | # seguinte linha. Esta função agora está alterando o nível do módulo 659 | # estado e introduzindo um efeito colateral! 660 | global fullname 661 | fullname = fullname.split() 662 | 663 | split_into_first_and_last_name() 664 | 665 | # MyPy irá detectar o problema, 'Incompatible types in 666 | # assignment: (expression has type "List[str]", variable has type "str")' 667 | print(fullname) # ["Ryan", "McDermott"] 668 | 669 | # OK. Funcionou da primeira vez, mas o que acontecerá se chamarmos de 670 | # funcionar de novo? 671 | ``` 672 | 673 | **Bom:** 674 | ```python 675 | from typing import List, AnyStr 676 | 677 | 678 | def split_into_first_and_last_name(name: AnyStr) -> List[AnyStr]: 679 | return name.split() 680 | 681 | fullname = "Ryan McDermott" 682 | name, surname = split_into_first_and_last_name(fullname) 683 | 684 | print(name, surname) # => Ryan McDermott 685 | ``` 686 | 687 | **Muito bom** 688 | ```python 689 | from typing import Text 690 | from dataclasses import dataclass 691 | 692 | 693 | @dataclass 694 | class Person: 695 | name: Text 696 | 697 | @property 698 | def name_as_first_and_last(self) -> list: 699 | return self.name.split() 700 | 701 | 702 | # A razão pela qual criamos instâncias de classes é para gerenciar o estado! 703 | person = Person("Ryan McDermott") 704 | print(person.name) # => "Ryan McDermott" 705 | print(person.name_as_first_and_last) # => ["Ryan", "McDermott"] 706 | ``` 707 | 708 | **[⬆ back to top](#índice)** 709 | 710 | ## **Objetos e Estruturas de Dados** 711 | 712 | *Coming soon* 713 | 714 | **[⬆ back to top](#índice)** 715 | 716 | ## **Classes** 717 | 718 | ### **Princípio da Responsabilidade Única (SRP)** 719 | ### **Princípio do Aberto/Fechado (OCP)** 720 | ### **Princípio de Substituição de Liskov (LSP)** 721 | ### **Princípio da Segregação de Interface (ISP)** 722 | ### **Princípio da Inversão de Dependência (DIP)** 723 | 724 | *Coming soon* 725 | 726 | **[⬆ back to top](#índice)** 727 | 728 | ## **Não se repita (DRY)** 729 | 730 | Mais informações sobre o príncipio [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). 731 | 732 | Como programador, você deve evitar código duplicado. A duplicação é ruim porque isso significa 733 | que há mais de um lugar para alterar algo, se precisar mudar alguma lógica 734 | 735 | Imagine que você é dono de um restaurante e você toma conta do seu estoque: todos os seus tomates, cebolas, alhos, temperos, etc. Se você tem multiplas listas onde guarda estas informações, então você terá que atualizar todas elas quando servir um prato que tenha tomates. Se você tivesse apenas uma lista, teria apenas um lugar para atualizar! 736 | 737 | Frequentemente, você possui código duplicado porque você tem duas ou mais coisas levemente diferentes, que possuem muito em comum, mas suas diferenças lhe forçam a ter mais duas ou três funções que fazem muito das mesmas coisas. Remover código duplicado significa criar uma abstração que seja capaz de lidar com este conjunto de coisas diferentes com apenas uma função/módulo/classe. 738 | 739 | Conseguir a abstração correta é crítico, por isso que você deveria seguir os princípios SOLID descritos na seção Classes. Abstrações ruins podem ser piores do que código duplicado, então tome cuidado! Dito isto, se você puder fazer uma boa abstração, faça-a! Não repita a si mesmo, caso contrário você se pegará atualizando muitos lugares toda vez que precisar mudar qualquer coisinha. 740 | 741 | **Ruim:** 742 | 743 | ```python 744 | from typing import List, Text, Dict 745 | from dataclasses import dataclass 746 | 747 | @dataclass 748 | class Developer: 749 | def __init__(self, experience: float, github_link: Text) -> None: 750 | self._experience = experience 751 | self._github_link = github_link 752 | 753 | @property 754 | def experience(self) -> float: 755 | return self._experience 756 | 757 | @property 758 | def github_link(self) -> Text: 759 | return self._github_link 760 | 761 | @dataclass 762 | class Manager: 763 | def __init__(self, experience: float, github_link: Text) -> None: 764 | self._experience = experience 765 | self._github_link = github_link 766 | 767 | @property 768 | def experience(self) -> float: 769 | return self._experience 770 | 771 | @property 772 | def github_link(self) -> Text: 773 | return self._github_link 774 | 775 | 776 | def get_developer_list(developers: List[Developer]) -> List[Dict]: 777 | developers_list = [] 778 | for developer in developers: 779 | developers_list.append({ 780 | 'experience' : developer.experience, 781 | 'github_link' : developer.github_link 782 | }) 783 | return developers_list 784 | 785 | def get_manager_list(managers: List[Manager]) -> List[Dict]: 786 | managers_list = [] 787 | for manager in managers: 788 | managers_list.append({ 789 | 'experience' : manager.experience, 790 | 'github_link' : manager.github_link 791 | }) 792 | return managers_list 793 | 794 | ## create list objects of developers 795 | company_developers = [ 796 | Developer(experience=2.5, github_link='https://github.com/1'), 797 | Developer(experience=1.5, github_link='https://github.com/2') 798 | ] 799 | company_developers_list = get_developer_list(developers=company_developers) 800 | 801 | ## create list objects of managers 802 | company_managers = [ 803 | Manager(experience=4.5, github_link='https://github.com/3'), 804 | Manager(experience=5.7, github_link='https://github.com/4') 805 | ] 806 | company_managers_list = get_manager_list(managers=company_managers) 807 | ``` 808 | 809 | **Bom:** 810 | 811 | ```python 812 | from typing import List, Text, Dict 813 | from dataclasses import dataclass 814 | 815 | @dataclass 816 | class Employee: 817 | def __init__(self, experience: float, github_link: Text) -> None: 818 | self._experience = experience 819 | self._github_link = github_link 820 | 821 | @property 822 | def experience(self) -> float: 823 | return self._experience 824 | 825 | @property 826 | def github_link(self) -> Text: 827 | return self._github_link 828 | 829 | 830 | 831 | def get_employee_list(employees: List[Employee]) -> List[Dict]: 832 | employees_list = [] 833 | for employee in employees: 834 | employees_list.append({ 835 | 'experience' : employee.experience, 836 | 'github_link' : employee.github_link 837 | }) 838 | return employees_list 839 | 840 | ## create list objects of developers 841 | company_developers = [ 842 | Employee(experience=2.5, github_link='https://github.com/1'), 843 | Employee(experience=1.5, github_link='https://github.com/2') 844 | ] 845 | company_developers_list = get_employee_list(employees=company_developers) 846 | 847 | ## create list objects of managers 848 | company_managers = [ 849 | Employee(experience=4.5, github_link='https://github.com/3'), 850 | Employee(experience=5.7, github_link='https://github.com/4') 851 | ] 852 | company_managers_list = get_employee_list(employees=company_managers) 853 | ``` 854 | 855 | 856 | 857 | **[⬆ back to top](#índice)** 858 | 859 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import re 3 | import time 4 | import typing 5 | 6 | import pytest 7 | from mypy import api 8 | 9 | code_rxp = re.compile('```python(.*?)```', re.DOTALL | re.MULTILINE) 10 | 11 | 12 | class MyPyValidationError(BaseException): 13 | """A validation error occurred when MyPy attempted to validate the code""" 14 | 15 | 16 | def fake_print(*args, **kwargs): 17 | """Dummy replacement for print() that does nothing""" 18 | pass 19 | 20 | 21 | def pytest_collect_file(parent, path): 22 | """Collect all file suitable for use in tests""" 23 | if path.basename == "README.md": 24 | return ReadmeFile.from_parent(parent, fspath=path) 25 | 26 | 27 | class ReadmeFile(pytest.File): 28 | """A Markdown formatted readme file containing code snippets""" 29 | 30 | def collect(self): 31 | """Collect all code snippets""" 32 | raw_text = self.fspath.open().read() 33 | for idx, code in enumerate(code_rxp.findall(raw_text), 1): 34 | yield ReadmeItem.from_parent( 35 | self, name=str(idx), spec=code.strip() 36 | ) 37 | 38 | 39 | def _with_patched_sleep(func, *args, **kwargs): 40 | """Patch the sleep function so that it does nothing""" 41 | _sleep = time.sleep 42 | time.sleep = lambda *args: None 43 | try: 44 | return func(*args, **kwargs) 45 | finally: 46 | time.sleep = _sleep 47 | 48 | 49 | class ReadmeItem(pytest.Item): 50 | """A readme test item that validates a code snippet""" 51 | builtins = ( 52 | ('typing', typing), 53 | ('datetime', importlib.import_module('datetime')), 54 | ('hashlib', importlib.import_module('hashlib')), 55 | ('print', fake_print) 56 | ) 57 | 58 | def __init__(self, name, parent, spec): 59 | super().__init__(name, parent) 60 | self.spec = spec 61 | 62 | def runtest(self): 63 | """Run the test""" 64 | builtins = dict(self.builtins) 65 | byte_code = compile(self.spec, '', 'exec') 66 | _with_patched_sleep(exec, byte_code, builtins) 67 | msg, _, error = api.run(['--no-color-output', '-c', self.spec]) 68 | if error: 69 | # Ignore missing errors related to the injected names 70 | for name in builtins: 71 | if f"Name '{name}' is not defined" in msg: 72 | break 73 | else: 74 | raise MyPyValidationError(msg) 75 | 76 | def repr_failure(self, excinfo, **kwargs): 77 | """ called when self.runtest() raises an exception. """ 78 | return ( 79 | f"Code snippet {self.name} raised an error: {excinfo.value}. " 80 | f"The executed code was: {self.spec}" 81 | ) 82 | 83 | def reportinfo(self): 84 | """Report some basic information on the test outcome""" 85 | return self.fspath, 0, "usecase: {}".format(self.name) 86 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mypy 3 | --------------------------------------------------------------------------------