clean-code-python

roo333·2022년 3월 2일
0
post-thumbnail

title: "Clean-Code-Python"
date: 2022-03-02T15:05:27+09:00
tags: ["Python"]
author: ["신승혁"]


원작자에게 허락을 맡고 개인적으로 번역한 글입니다. 출처

clean-code-python


목차

  1. 소개
  2. 변수
  3. 함수
  4. 객체와 자료구조
  5. 클래스
    1. S: Single Responsibility Principle (SRP)
    2. O: Open/Closed Principle (OCP)
    3. L: Liskov Substitution Principle (LSP)
    4. I: Interface Segregation Principle (ISP)
    5. D: Dependency Inversion Principle (DIP)
  6. Don't repeat yourself (DRY)

소개

소프트웨어 엔지니어링의 원칙인 로버트 마틴의 클린코드를 파이썬에 적용한 내용입니다. 파이썬에서 읽을 수 있고 재사용 가능하며 리팩터블할 수 있는 소프트웨어를 만드는 안내서입니다.

여기에 있는 모든 원칙이 엄격하게 지켜질 필요는 없으며, 더 적은 수의 원칙을 보편적으로 사용할 수 있습니다. 여기에 소개된 내용은 가이드라인일 뿐 그 이상은 아니지만, 클린 코드 저자들에 의해 수년간의 집단 경험을 통해 문서화 된것입니다.

clean-code-javascript에서 영감을 받았습니다.

Python3.7+ 대상

변수

의미 있고 발음 가능한 변수 이름 사용

Bad:

import datetime


ymdstr = datetime.date.today().strftime("%y-%m-%d")

Good:

import datetime


current_date: str = datetime.date.today().strftime("%y-%m-%d")

⬆ back to top

같은 유형의 변수에 동일한 어휘 사용

Bad:
이 예에서는 동일한 기본 엔티티에 대해 세 가지 다른 이름을 사용합니다:

def get_user_info(): pass
def get_client_data(): pass
def get_customer_record(): pass

Good:
엔티티가 동일하면 기능에서 해당 엔티티를 일관되게 참조해야 합니다:

def get_user_info(): pass
def get_user_data(): pass
def get_user_record(): pass

Even better
파이썬은 (또한) 객체 지향 프로그래밍 언어이기도 합니다. 타당하다면 구체적인 구현과 함께 기능을 패키징합니다.
인스턴스(instance)의 프로퍼티, 프로퍼티 메서드 또는 메서드로:

from typing import Union, Dict


class Record:
    pass


class User:
    info : str

    @property
    def data(self) -> Dict[str, str]:
        return {}

    def get_record(self) -> Union[Record, None]:
        return Record()

⬆ back to top

검색 가능한 이름 사용

우리는 우리가 코드를 작성하는 시간보다 읽는 시간이 더 많을겁니다. 우리가 읽을수 있고 검색가능한 코드를 만드는것은 중요합니다.

프로그램의 의미가 있는 변수들의 이름을 붙히지 않음으로써 우리는 코드를 읽는 사람들에게 상처를 주곤합니다.

이름을 검색 가능하게 만드세요.

Bad:

import time


# What is the number 86400 for again?
time.sleep(86400)

Good:

import time


# Declare them in the global namespace for the module.
SECONDS_IN_A_DAY = 60 * 60 * 24
time.sleep(SECONDS_IN_A_DAY)

⬆ back to top

설명 변수 사용

Bad:

import re


address = "One Infinite Loop, Cupertino 95014"
city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$"

matches = re.match(city_zip_code_regex, address)
if matches:
    print(f"{matches[1]}: {matches[2]}")

Not bad:

더 좋긴 하지만, 여전히 정규식에 많이 의존하고 있습니다.

import re


address = "One Infinite Loop, Cupertino 95014"
city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$"
matches = re.match(city_zip_code_regex, address)

if matches:
    city, zip_code = matches.groups()
    print(f"{city}: {zip_code}")

Good:

하위 패턴의 이름을 지정하여 정규식에 대한 의존성을 줄입니다.

import re


address = "One Infinite Loop, Cupertino 95014"
city_zip_code_regex = r"^[^,\\]+[,\\\s]+(?P<city>.+?)\s*(?P<zip_code>\d{5})?$"

matches = re.match(city_zip_code_regex, address)
if matches:
    print(f"{matches['city']}, {matches['zip_code']}")

⬆ back to top

Mental Mapping 방지

변수가 의미하는 바를 당신의 코드를 읽는 사람에게 번역하도록 강요하지 마세요.
명시하는 것이 암묵적인 것보다 낫습니다.

Bad:

seq = ("Austin", "New York", "San Francisco")

for item in seq:
    #do_stuff()
    #do_some_other_stuff()

    # Wait, what's `item` again?
    print(item)

Good:

locations = ("Austin", "New York", "San Francisco")

for location in locations:
    #do_stuff()
    #do_some_other_stuff()
    # ...
    print(location)

⬆ back to top

불필요한 컨텍스트 추가 안 함

클래스/객체 이름이 알려주는 내용이 있으면 변수 이름에 이 내용을 반복하지 마십시오.

Bad:

class Car:
    car_make: str
    car_model: str
    car_color: str

Good:

class Car:
    make: str
    model: str
    color: str

⬆ back to top

단락 또는 조건 대신 기본 인수 사용

까다롭게

왜 이렇게 작성합니까?:

import hashlib


def create_micro_brewery(name):
    name = "Hipster Brew Co." if name is None else name
    slug = hashlib.sha1(name.encode()).hexdigest()
    # etc.

... 기본인수를 지정할수 있는데, 또한 인수로 문자열을 받을 것을 명확히 합니다.

Good:

import hashlib


def create_micro_brewery(name: str = "Hipster Brew Co."):
    slug = hashlib.sha1(name.encode()).hexdigest()
    # etc.

⬆ back to top

함수

함수 인수(2개 이하가 이상적입니다)

함수 매개 변수의 양을 제한하는 것은 함수를 더 쉽게 테스트할 수 있기 때문에 매우 중요합니다. 세 개 이상이면 조합 폭발로 이어지며, 각각의 개별 논증으로 수많은 다른 사례를 테스트해야 합니다.

인수가 하나도 없는 것이 이상적인 경우입니다. 1~2개의 인수느 괜찮고, 세개는 피해야 하고 그 이상의 것은 통합되어야 합니다.(객체 등으로). 보통, 만약 여러분이 두 개 이상의 인수를 가지고 있다면, 여러분의 기능은 너무 많은 것을 하려고 하는 것입니다. 그렇지 않은 경우에는 대부분 더 높은 수준의 객체가 인수로 충분합니다.

Bad:

def create_menu(title, body, button_text, cancellable):
    pass

Java-esque:

class Menu:
    def __init__(self, config: dict):
        self.title = config["title"]
        self.body = config["body"]
        # ...

menu = Menu(
    {
        "title": "My Menu",
        "body": "Something about my menu",
        "button_text": "OK",
        "cancellable": False
    }
)

Also good

class MenuConfig:
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool = False


def create_menu(config: MenuConfig) -> None:
    title = config.title
    body = config.body
    # ...


config = MenuConfig()
config.title = "My delicious menu"
config.body = "A description of the various items on the menu"
config.button_text = "Order now!"
# The instance attribute overrides the default class attribute.
config.cancellable = True

create_menu(config)

Fancy

from typing import NamedTuple


class MenuConfig(NamedTuple):
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool = False


def create_menu(config: MenuConfig):
    title, body, button_text, cancellable = config
    # ...


create_menu(
    MenuConfig(
        title="My delicious menu",
        body="A description of the various items on the menu",
        button_text="Order now!"
    )
)

Even fancier

from dataclasses import astuple, dataclass


@dataclass
class MenuConfig:
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool = False

def create_menu(config: MenuConfig):
    title, body, button_text, cancellable = astuple(config)
    # ...


create_menu(
    MenuConfig(
        title="My delicious menu",
        body="A description of the various items on the menu",
        button_text="Order now!"
    )
)

Even fancier, Python3.8+ only

from typing import TypedDict


class MenuConfig(TypedDict):
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool


def create_menu(config: MenuConfig):
    title = config["title"]
    # ...


create_menu(
    # You need to supply all the parameters
    MenuConfig(
        title="My delicious menu",
        body="A description of the various items on the menu",
        button_text="Order now!",
        cancellable=True
    )
)

⬆ back to top

함수는 한 가지 일을 해야 한다.

이것은 소프트웨어 공학에서 단연코 가장 중요한 규칙입니다. 함수가 한 가지 이상의 일을 할 때는 구성, 테스트, 추론하기가 더 어려워진다. 함수를 하나의 동작으로 분리할 수 있으면 쉽게 리팩터링할 수 있으며 코드가 훨씬 깔끔하게 읽힙니다.

이 가이드에서 이것만 지키더라도, 당신은 많은 개발자보다 앞서게 될 것입니다.

Bad:

from typing import List


class Client:
    active: bool


def email(client: Client) -> None:
    pass


def email_clients(clients: List[Client]) -> None:
    """Filter active clients and send them an email.
    """
    for client in clients:
        if client.active:
            email(client)

Good:

from typing import List


class Client:
    active: bool


def email(client: Client) -> None:
    pass


def get_active_clients(clients: List[Client]) -> List[Client]:
    """Filter active clients.
    """
    return [client for client in clients if client.active]


def email_clients(clients: List[Client]) -> None:
    """Send an email to a given list of clients.
    """
    for client in get_active_clients(clients):
        email(client)

이제 제너레이터를 사용할 수 있는 기회가 보이나요?

Even better

from typing import Generator, Iterator


class Client:
    active: bool


def email(client: Client):
    pass


def active_clients(clients: Iterator[Client]) -> Generator[Client, None, None]:
    """Only active clients"""
    return (client for client in clients if client.active)


def email_client(clients: Iterator[Client]) -> None:
    """Send an email to a given list of clients.
    """
    for client in active_clients(clients):
        email(client)

⬆ back to top

함수 이름에는 해당 기능이 명시되어 있어야 합니다.

Bad:

class Email:
    def handle(self) -> None:
        pass

message = Email()
# What is this supposed to do again?
message.handle()

Good:

class Email:
    def send(self) -> None:
        """Send this message"""

message = Email()
message.send()

⬆ back to top

함수는 추상화의 한 수준이어야 합니다

당신이 하나 이상의 추상화를 가지고 있을 때, 당신의 함수는 보통 너무 많은 일을 하고 있습니다.
함수를 분할하면 재사용이 가능하고 테스트가 쉬워집니다.

(*역: 예로 마틴 파울러의 리팩토링 책에서는 단 한 줄짜리 함수도 규칙에 맞게 분리된다면 문제가 없다고 이야기하고 있습니다.)

Bad:

# type: ignore

def parse_better_js_alternative(code: str) -> None:
    regexes = [
        # ...
    ]

    statements = code.split('\n')
    tokens = []
    for regex in regexes:
        for statement in statements:
            pass

    ast = []
    for token in tokens:
        pass

    for node in ast:
        pass

Good:

from typing import Tuple, List, Dict


REGEXES: Tuple = (
   # ...
)


def parse_better_js_alternative(code: str) -> None:
    tokens: List = tokenize(code)
    syntax_tree: List = parse(tokens)

    for node in syntax_tree:
        pass


def tokenize(code: str) -> List:
    statements = code.split()
    tokens: List[Dict] = []
    for regex in REGEXES:
        for statement in statements:
            pass

    return tokens


def parse(tokens: List) -> List:
    syntax_tree: List[Dict] = []
    for token in tokens:
        pass

    return syntax_tree

⬆ back to top

플래그를 함수 매개 변수로 사용하지 말 것

플래그는 사용자에게 이 기능이 하나 이상의 기능을 수행함을 알려줍니다. 함수는 한 가지 일을 해야 한다. 함수가 Boolean을 기준으로 다른 코드 경로를 따르는 경우 함수를 분할합니다.

(*역: SOLID 원칙에서 Single-responsibility Principle (SRP)에 해당하는 내용입니다.)

Bad:

from tempfile import gettempdir
from pathlib import Path


def create_file(name: str, temp: bool) -> None:
    if temp:
        (Path(gettempdir()) / name).touch()
    else:
        Path(name).touch()

Good:

from tempfile import gettempdir
from pathlib import Path


def create_file(name: str) -> None:
    Path(name).touch()


def create_temp_file(name: str) -> None:
    (Path(gettempdir()) / name).touch()

⬆ back to top

사이드 이펙트(부작용)를 피해라. (*역: 명령-질의 원칙의 내용입니다.)

함수는 값을 가져다가 다른 값을 반환하는 것 외에 다른 작업을 하면 부작용이 발생합니다. 부작용의 예로는, 파일을 쓸때나, 전역 변수를 수정하거나, 실수로 모든 돈을 낯선 사람에게 송금하는 것일 수 있습니다.

그러나 때때로 예를 들어 이전 예제에서와 프로그램에 부작용이 있을 수 밖에 없는 경우가 있습니다. 오류를 파일에 써야 할 수도 있습니다. 이러한 경우 부작용을 통합하는 위치를 중앙에 표시하고 표시해야 합니다. 특정 파일에 쓰는 여러 기능과 클래스로 구현하는것보단 하나의 서비스로 구현해야 합니다.(*역: 응집력에 관한 이야기 입니다.)

요점은 구조 없이 객체 간에 상태를 공유하거나, 무엇이든 쓸 수 있는 변경 가능한 데이터 유형을 사용하거나, 클래스의 인스턴스를 사용하고, 부작용이 발생하는 곳을 중앙 집중화하지 않는 것과 같은 일반적인 함정을 피하는 것입니다.

이 규칙을 지킬수만 있다면 대다수의 다른 프로그래머보다 더 행복할 것입니다.

Bad:

# type: ignore

# This is a module-level name.
# It's good practice to define these as immutable values, such as a string.
# However...
fullname = "Ryan McDermott"

def split_into_first_and_last_name() -> None:
    # The use of the global keyword here is changing the meaning of the
    # the following line. This function is now mutating the module-level
    # state and introducing a side-effect!
    global fullname
    fullname = fullname.split()

split_into_first_and_last_name()

# MyPy will spot the problem, complaining about 'Incompatible types in
# assignment: (expression has type "List[str]", variable has type "str")'
print(fullname)  # ["Ryan", "McDermott"]

# OK. It worked the first time, but what will happen if we call the
# function again?

Good:

from typing import List, AnyStr


def split_into_first_and_last_name(name: AnyStr) -> List[AnyStr]:
    return name.split()

fullname = "Ryan McDermott"
name, surname = split_into_first_and_last_name(fullname)

print(name, surname)  # => Ryan McDermott

Also good

from dataclasses import dataclass


@dataclass
class Person:
    name: str

    @property
    def name_as_first_and_last(self) -> list:
        return self.name.split()


# The reason why we create instances of classes is to manage state!
person = Person("Ryan McDermott")
print(person.name)  # => "Ryan McDermott"
print(person.name_as_first_and_last)  # => ["Ryan", "McDermott"]

⬆ back to top

객체와 자료구조

Coming soon

⬆ back to top

클래스

Single Responsibility Principle (SRP)

Open/Closed Principle (OCP)

Liskov Substitution Principle (LSP)

Interface Segregation Principle (ISP)

Dependency Inversion Principle (DIP)

Coming soon

⬆ back to top

Don't repeat yourself (DRY)

DRY원칙을 지키기 위해 노력하세요.

코드가 중복되지 않도록 최선을 다하세요. 중복 코드는 어떤 논리를 바꿔야 할 경우 변경할 곳이 두 곳 이상 있다는 것을 의미하기 때문에 좋지 않습니다.

당신이 식당을 운영하고 당신의 모든 토마토, 양파, 마늘, 향신료 등 당신의 재고 목록을 추적한다고 상상해 보세요. 만약 당신이 이것을 보관하고 있는 여러 목록이 있다면, 당신이 토마토가 들어있는 요리를 제공할 때 모든 목록이 업데이트되어야 합니다. 목록이 하나뿐이면 업데이트할 장소가 하나뿐입니다!

두 개 이상의 약간 다른 공통점을 가지고 있기 때문에 중복 코드가 있는 경우가 많지만, 그 차이점 때문에 어쩔 수 없습니다.
거의 같은 일을 하는 두 개 이상의 분리된 기능을 가지고 있습니다. 중복 코드를 제거하는 것은 하나의 함수/모듈/클래스만으로 이러한 다른 것들을 처리할 수 있는 추상화를 만드는 것을 의미합니다.

추상화를 올바르게 하는 것은 매우 중요합니다. 잘못된 추상화는 중복 코드보다 더 나쁠 수 있으므로 주의해야합니다! 하지만 할 수 있다면
추상화를 시도해보는 것도 좋습니다!

반복하지 마십시오, 그렇지 않으면 한 가지를 변경하고 싶을 때마다 여러 장소를 업데이트하고 있는 자신을 발견할 수 있습니다.

*역 :<클린 아키텍처> 책에서 이에 대한 좋은 내용을 언급하는데, 그 내용은 중복에도 종류가 있다는 것이다.

  • 진짜 중복 :
    한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야한다.
  • 우발적 중복(거짓된 중복) :
    중복으로 보이는 두 코드의 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다.

Bad:

from typing import List, Dict
from dataclasses import dataclass

@dataclass
class Developer:
    def __init__(self, experience: float, github_link: str) -> None:
        self._experience = experience
        self._github_link = github_link
        
    @property
    def experience(self) -> float:
        return self._experience
    
    @property
    def github_link(self) -> str:
        return self._github_link
    
@dataclass
class Manager:
    def __init__(self, experience: float, github_link: str) -> None:
        self._experience = experience
        self._github_link = github_link
        
    @property
    def experience(self) -> float:
        return self._experience
    
    @property
    def github_link(self) -> str:
        return self._github_link
    

def get_developer_list(developers: List[Developer]) -> List[Dict]:
    developers_list = []
    for developer in developers:
        developers_list.append({
        'experience' : developer.experience,
        'github_link' : developer.github_link
            })
    return developers_list

def get_manager_list(managers: List[Manager]) -> List[Dict]:
    managers_list = []
    for manager in managers:
        managers_list.append({
        'experience' : manager.experience,
        'github_link' : manager.github_link
            })
    return managers_list

## create list objects of developers
company_developers = [
    Developer(experience=2.5, github_link='https://github.com/1'),
    Developer(experience=1.5, github_link='https://github.com/2')
]
company_developers_list = get_developer_list(developers=company_developers)

## create list objects of managers
company_managers = [
    Manager(experience=4.5, github_link='https://github.com/3'),
    Manager(experience=5.7, github_link='https://github.com/4')
]
company_managers_list = get_manager_list(managers=company_managers)

Good:

from typing import List, Dict
from dataclasses import dataclass

@dataclass
class Employee:
    def __init__(self, experience: float, github_link: str) -> None:
        self._experience = experience
        self._github_link = github_link
        
    @property
    def experience(self) -> float:
        return self._experience
    
    @property
    def github_link(self) -> str:
        return self._github_link
    


def get_employee_list(employees: List[Employee]) -> List[Dict]:
    employees_list = []
    for employee in employees:
        employees_list.append({
        'experience' : employee.experience,
        'github_link' : employee.github_link
            })
    return employees_list

## create list objects of developers
company_developers = [
    Employee(experience=2.5, github_link='https://github.com/1'),
    Employee(experience=1.5, github_link='https://github.com/2')
]
company_developers_list = get_employee_list(employees=company_developers)

## create list objects of managers
company_managers = [
    Employee(experience=4.5, github_link='https://github.com/3'),
    Employee(experience=5.7, github_link='https://github.com/4')
]
company_managers_list = get_employee_list(employees=company_managers)

⬆ back to top

0개의 댓글