깨끗한 코드 보장: 매개변수화된 Python 살펴보기

게시 됨: 2022-03-11

이 게시물에서 저는 깨끗한 Pythonic 코드를 생성하는 데 가장 중요한 기술 또는 패턴이라고 생각하는 것, 즉 매개변수화에 대해 이야기할 것입니다. 이 게시물은 다음과 같은 경우에 적합합니다.

  • 당신은 전체 디자인 패턴에 비교적 익숙하지 않으며 패턴 이름과 클래스 다이어그램의 긴 목록에 약간 당황할 수 있습니다. 좋은 소식은 Python에 대해 절대적으로 알아야 하는 디자인 패턴이 단 하나뿐이라는 것입니다. 더 좋은 점은 이미 알고 있을 수도 있지만 모든 방법을 적용할 수 있는 것은 아닙니다.
  • Java 또는 C#과 같은 다른 OOP 언어에서 Python으로 왔으며 해당 언어의 디자인 패턴에 대한 지식을 Python으로 변환하는 방법을 알고 싶습니다. Python 및 기타 동적으로 유형이 지정된 언어에서 정적으로 유형이 지정된 OOP 언어에서 일반적인 많은 패턴은 저자 Peter Norvig가 말했듯이 "보이지 않거나 더 단순"합니다.

이 기사에서 우리는 "매개변수화"의 적용과 그것이 의존성 주입 , 전략 , 템플릿 메소드 , 추상 팩토리 , 팩토리 메소드데코레이터 로 알려진 주류 디자인 패턴과 어떻게 관련될 수 있는지 탐구할 것입니다. 파이썬에서 이들 중 많은 것들이 단순해지거나 파이썬의 매개변수가 호출 가능한 객체나 클래스가 될 수 있다는 사실 때문에 불필요하게 됩니다.

매개 변수화는 코드를 일반화하기 위해 함수 또는 메서드 내에 정의된 값 또는 개체를 가져와서 해당 함수 또는 메서드에 대한 매개 변수로 만드는 프로세스입니다. 이 프로세스는 "매개변수 추출" 리팩토링이라고도 합니다. 어떤 면에서 이 기사는 디자인 패턴과 리팩토링에 관한 것입니다.

매개변수화된 Python의 가장 간단한 경우

대부분의 예제에서 일부 그래픽을 수행하기 위해 교육용 표준 라이브러리 거북이 모듈을 사용할 것입니다.

다음은 turtle 를 사용하여 100x100 정사각형을 그리는 코드입니다.

 from turtle import Turtle turtle = Turtle() for i in range(0, 4): turtle.forward(100) turtle.left(90)

이제 다른 크기의 정사각형을 그리려고 한다고 가정합니다. 이 시점에서 아주 주니어 프로그래머는 이 블록을 복사하여 붙여넣고 수정하려는 유혹을 받을 것입니다. 분명히 훨씬 더 나은 방법은 먼저 정사각형 그리기 코드를 함수로 추출한 다음 정사각형의 크기를 이 함수의 매개변수로 만드는 것입니다.

 def draw_square(size): for i in range(0, 4): turtle.forward(size) turtle.left(90) draw_square(100)

이제 draw_square 를 사용하여 모든 크기의 사각형을 그릴 수 있습니다. 이것이 매개변수화의 필수 기술의 전부이며 복사-붙여넣기 프로그래밍을 제거하는 첫 번째 주요 사용법을 보았습니다.

위 코드의 즉각적인 문제는 draw_square 가 전역 변수에 의존한다는 것입니다. 이것은 많은 나쁜 결과를 가져오고 그것을 고칠 수 있는 두 가지 쉬운 방법이 있습니다. 첫 번째는 draw_squareTurtle 인스턴스 자체를 생성하는 것입니다(나중에 논의하겠습니다). 모든 그림에 대해 하나의 Turtle 를 사용하려는 경우 이는 바람직하지 않을 수 있습니다. 그래서 지금은 단순히 매개변수화를 다시 사용하여 turtledraw_square 에 대한 매개변수로 만들 것입니다.

 from turtle import Turtle def draw_square(turtle, size): for i in range(0, 4): turtle.forward(size) turtle.left(90) turtle = Turtle() draw_square(turtle, 100)

이것은 종속성 주입이라는 멋진 이름을 가지고 있습니다. 함수가 작업을 수행하기 위해 일종의 객체가 필요한 경우(예: draw_squareTurtle 이 필요한 경우) 호출자는 해당 객체를 매개변수로 전달해야 합니다. 아니요, 정말로, Python 종속성 주입에 대해 궁금한 적이 있다면 이것이 전부입니다.

지금까지 우리는 매우 기본적인 두 가지 사용법을 다루었습니다. 이 기사의 나머지 부분에 대한 주요 관찰 사항은 Python에는 다른 언어보다 매개변수가 될 수 있는 범위가 매우 넓으며 이는 Python을 매우 강력한 기술로 만든다는 것입니다.

객체인 모든 것

Python에서는 이 기술을 사용하여 객체인 모든 것을 매개변수화할 수 있으며 Python에서는 실제로 접하는 대부분이 객체입니다. 여기에는 다음이 포함됩니다.

  • 문자열 "I'm a string" 및 정수 42 또는 사전과 같은 내장 유형의 인스턴스
  • 다른 유형 및 클래스의 인스턴스(예: datetime.datetime 객체)
  • 기능 및 방법
  • 기본 제공 유형 및 사용자 정의 클래스

마지막 두 가지는 가장 놀라운 것입니다. 특히 다른 언어에서 온 경우 더 많은 토론이 필요합니다.

매개변수로서의 기능

Python의 function 문은 두 가지 작업을 수행합니다.

  1. 함수 객체를 생성합니다.
  2. 해당 개체를 가리키는 로컬 범위에 이름을 만듭니다.

REPL에서 다음 개체로 재생할 수 있습니다.

 > >> def foo(): ... return "Hello from foo" > >> > >> foo() 'Hello from foo' > >> print(foo) <function foo at 0x7fc233d706a8> > >> type(foo) <class 'function'> > >> foo.name 'foo'

모든 객체와 마찬가지로 다른 변수에 함수를 할당할 수 있습니다.

 > >> bar = foo > >> bar() 'Hello from foo'

bar 는 동일한 객체의 다른 이름이므로 이전과 동일한 내부 __name__ 속성을 갖습니다.

 > >> bar.name 'foo' > >> bar <function foo at 0x7fc233d706a8>

그러나 중요한 점은 함수는 단지 객체이기 때문에 함수가 사용되는 곳이라면 어디에서나 매개변수가 될 수 있다는 것입니다.

따라서 위의 사각형 그리기 기능을 확장하고 이제 사각형을 그릴 때 각 모서리에서 일시 중지하고 싶은 경우가 있다고 가정합니다. time.sleep() 에 대한 호출입니다.

그러나 때때로 우리가 일시 중지하고 싶지 않다고 가정해 보십시오. 이를 달성하는 가장 간단한 방법은 기본적으로 일시 중지하지 않도록 기본값이 0인 pause 매개변수를 추가하는 것입니다.

그러나 나중에 우리는 코너에서 완전히 다른 것을 하고 싶을 때가 있다는 것을 나중에 알게 됩니다. 아마도 우리는 각 모서리에 다른 모양을 그리고 펜 색상을 변경하고 싶을 것입니다. 우리는 우리가 해야 할 일마다 하나씩 더 많은 매개변수를 추가하고 싶을 것입니다. 그러나 훨씬 더 좋은 솔루션은 수행할 작업으로 모든 기능을 전달할 수 있도록 하는 것입니다. 기본적으로 우리는 아무것도 하지 않는 함수를 만들 것입니다. 또한 필요한 경우 이 함수가 로컬 turtlesize 매개변수를 허용하도록 만들 것입니다.

 def do_nothing(turtle, size): pass def draw_square(turtle, size, at_corner=do_nothing): for i in range(0, 4): turtle.forward(size) at_corner(turtle, size) turtle.left(90) def pause(turtle, size): time.sleep(5) turtle = Turtle() draw_square(turtle, 100, at_corner=pause)

또는 각 모서리에 더 작은 사각형을 재귀적으로 그리는 것과 같이 좀 더 멋진 작업을 수행할 수 있습니다.

 def smaller_square(turtle, size): if size < 10: return draw_square(turtle, size / 2, at_corner=smaller_square) draw_square(turtle, 128, at_corner=smaller_square) 

위의 python 매개변수화된 코드에서 설명한 것처럼 재귀적으로 그려진 작은 사각형의 그림

물론 이것의 변형이 있습니다. 많은 예에서 함수의 반환 값이 사용됩니다. 여기에 더 명령적인 프로그래밍 스타일이 있으며 함수는 부작용에 대해서만 호출됩니다.

다른 언어로…

Python에서 일급 함수를 사용하면 이 작업을 매우 쉽게 수행할 수 있습니다. 그것들이 없는 언어나 매개변수에 대한 유형 서명이 필요한 일부 정적으로 유형이 지정된 언어에서는 이것이 더 어려울 수 있습니다. 일급 함수가 없다면 어떻게 해야 할까요?

한 가지 해결책은 draw_squareSquareDrawer 클래스로 바꾸는 것입니다.

 class SquareDrawer: def __init__(self, size): self.size = size def draw(self, t): for i in range(0, 4): t.forward(self.size) self.at_corner(t, size) t.left(90) def at_corner(self, t, size): pass

이제 SquareDrawer 의 하위 클래스를 만들고 필요한 작업을 수행하는 at_corner 메서드를 추가할 수 있습니다. 이 파이썬 패턴은 템플릿 메서드 패턴으로 알려져 있습니다. 기본 클래스는 전체 작업 또는 알고리즘의 모양을 정의하고 작업의 변형 부분은 하위 클래스에서 구현해야 하는 메서드에 넣습니다.

이것이 때때로 파이썬에서 도움이 될 수 있지만, 단순히 매개변수로 전달되는 함수로 변형 코드를 가져오는 것이 훨씬 더 간단할 때가 많습니다.

일급 함수가 없는 언어에서 이 문제에 접근할 수 있는 두 번째 방법은 다음과 같이 함수를 클래스 내부의 메서드로 래핑하는 것입니다.

 class DoNothing: def run(self, turtle, size): pass def draw_square(turtle, size, at_corner=DoNothing()): for i in range(0, 4): turtle.forward(size) at_corner.run(turtle, size) t.left(90) class Pauser: def run(self, turtle, size): time.sleep(5) draw_square(turtle, 100, at_corner=Pauser())

이를 전략 패턴이라고 합니다. 다시 말하지만, 이것은 특히 전략 클래스에 실제로 하나가 아닌 관련 함수 세트가 포함되어 있는 경우 Python에서 사용할 수 있는 유효한 패턴입니다. 그러나 종종 우리에게 정말로 필요한 것은 함수이고 우리는 클래스 작성을 중단할 수 있습니다.

기타 호출 가능

위의 예에서 함수를 매개변수로 다른 함수에 전달하는 것에 대해 이야기했습니다. 그러나 내가 쓴 모든 것은 사실 모든 호출 가능한 객체에 해당됩니다. 함수는 가장 간단한 예이지만 방법도 고려할 수 있습니다.

목록 foo 가 있다고 가정합니다.

 foo = [1, 2, 3]

foo 에는 이제 .append().count() ) 와 같은 많은 메서드가 연결되어 있습니다. 이러한 "바운드 메서드"는 전달되어 함수처럼 사용할 수 있습니다.

 > >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]

이러한 인스턴스 메서드 외에도 staticmethodsclassmethods , __call__ 을 구현하는 클래스의 인스턴스 및 클래스/유형 자체와 같은 다른 유형의 호출 가능한 객체가 있습니다.

매개변수로서의 클래스

파이썬에서 클래스는 "첫 번째 클래스"입니다. 즉, 사전, 문자열 등과 같은 런타임 객체입니다. 이것은 함수가 객체인 것보다 더 이상하게 보일 수 있지만 고맙게도 실제로는 함수보다 이 사실을 입증하는 것이 더 쉽습니다.

익숙한 class 문은 클래스를 만드는 좋은 방법이지만 유일한 방법은 아닙니다. 세 가지 인수 버전의 형식을 사용할 수도 있습니다. 다음 두 명령문은 정확히 동일한 작업을 수행합니다.

 class Foo: pass Foo = type('Foo', (), {})

두 번째 버전에서 우리가 방금 한 두 가지 일에 주목하십시오(class 문을 사용하여 더 편리하게 수행됨).

  1. 등호 오른쪽에 내부 이름이 Foo 인 새 클래스를 만들었습니다. 이것은 Foo.__name__ 을 수행하면 반환되는 이름입니다.
  2. 할당을 통해 우리는 우리가 방금 생성한 클래스 객체를 참조하는 Foo라는 이름을 현재 범위에 생성했습니다.

우리는 함수 문이 무엇을 하는지에 대해 동일한 관찰을 했습니다.

여기서 핵심은 클래스가 이름을 할당할 수 있는 객체라는 것입니다(즉, 변수에 넣을 수 있음). 사용 중인 클래스가 있는 곳이면 어디에서나 실제로는 사용 중인 변수만 볼 수 있습니다. 그리고 그것이 변수라면 매개변수가 될 수 있습니다.

우리는 이것을 여러 용도로 나눌 수 있습니다.

팩토리로서의 클래스

클래스는 자신의 인스턴스를 생성하는 호출 가능한 객체입니다.

 > >> class Foo: ... pass > >> Foo() <__main__.Foo at 0x7f73e0c96780>

그리고 객체로서 다른 변수에 할당할 수 있습니다.

 > >> myclass = Foo > >> myclass() <__main__.Foo at 0x7f73e0ca93c8>

위의 거북이 예제로 돌아가서 그리기에 거북이를 사용할 때의 한 가지 문제는 그림의 위치와 방향이 거북이의 현재 위치와 방향에 따라 달라지며 도움이 되지 않을 수 있는 다른 상태로 남을 수도 있다는 것입니다. 발신자. 이 문제를 해결하기 위해 draw_square 함수는 자체 거북이를 만들고 원하는 위치로 이동한 다음 사각형을 그릴 수 있습니다.

 def draw_square(x, y, size): turtle = Turtle() turtle.penup() # Don't draw while moving to the start position turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)

그러나 이제 사용자 정의 문제가 있습니다. 호출자가 거북이의 일부 속성을 설정하거나 인터페이스는 같지만 특별한 동작이 있는 다른 종류의 거북이를 사용하려고 한다고 가정해 보겠습니다.

이전처럼 종속성 주입으로 이 문제를 해결할 수 있습니다. 호출자는 Turtle 개체 설정을 담당합니다. 그러나 우리 함수가 때때로 다른 그리기 목적으로 많은 거북이를 만들어야 하거나 사각형의 한 면을 그리기 위해 각각 고유한 거북이가 있는 4개의 스레드를 시작하려는 경우에는 어떻게 될까요? 답은 단순히 Turtle 클래스를 함수에 대한 매개변수로 만드는 것입니다. 신경 쓰지 않는 호출자를 위해 간단하게 유지하기 위해 기본값과 함께 키워드 인수를 사용할 수 있습니다.

 def draw_square(x, y, size, make_turtle=Turtle): turtle = make_turtle() turtle.penup() turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)

이것을 사용하기 위해 거북이를 생성하고 수정하는 make_turtle 함수를 작성할 수 있습니다. 사각형을 그릴 때 거북이를 숨기고 싶다고 가정해 보겠습니다.

 def make_hidden_turtle(): turtle = Turtle() turtle.hideturtle() return turtle draw_square(5, 10, 20, make_turtle=make_hidden_turtle)

또는 Turtle 을 하위 클래스로 지정하여 해당 동작을 내장하고 하위 클래스를 매개변수로 전달할 수 있습니다.

 class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)

다른 언어로…

Java 및 C#과 같은 다른 여러 OOP 언어에는 일급 클래스가 없습니다. 클래스를 인스턴스화하려면 new 키워드 뒤에 실제 클래스 이름을 사용해야 합니다.

이 제한은 추상 팩토리(다른 클래스를 인스턴스화하는 것이 유일한 작업인 클래스 세트를 생성해야 함) 및 팩토리 메소드 패턴과 같은 패턴의 이유입니다. 보시다시피, Python에서는 클래스가 자체 팩토리이기 때문에 클래스를 매개변수로 가져오기만 하면 됩니다.

기본 클래스로서의 클래스

다른 클래스에 동일한 기능을 추가하기 위해 하위 클래스를 생성한다고 가정합니다. 예를 들어, 생성될 때 로그에 기록할 Turtle 하위 클래스가 필요합니다.

 import logging logger = logging.getLogger() class LoggingTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Turtle got created")

그러나 우리는 다른 클래스에서도 똑같은 일을 하고 있는 자신을 발견합니다.

 class LoggingHippo(Hippo): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Hippo got created")

이 둘 사이에 차이가 있는 유일한 사항은 다음과 같습니다.

  1. 기본 클래스
  2. 하위 클래스의 이름 — 하지만 우리는 그것에 대해 별로 신경 쓰지 않고 기본 클래스 __name__ 속성에서 자동으로 생성할 수 있습니다.
  3. debug 호출 내에서 사용된 이름입니다. 하지만 다시 기본 클래스 이름에서 생성할 수 있습니다.

단 하나의 변형만 있는 매우 유사한 두 개의 코드 비트에 직면했을 때 우리는 무엇을 할 수 있습니까? 첫 번째 예에서와 같이 함수를 만들고 변형 부분을 매개변수로 가져옵니다.

 def make_logging_class(cls): class LoggingThing(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("{0} got created".format(cls.__name__)) LoggingThing.__name__ = "Logging{0}".format(cls.__name__) return LoggingThing LoggingTurtle = make_logging_class(Turtle) LoggingHippo = make_logging_class(Hippo)

여기에 일류 클래스의 데모가 있습니다.

  • 클래스를 함수에 전달하고 매개변수에 일반적인 이름 cls 를 지정하여 키워드 class 와의 충돌을 방지했습니다(이 목적으로 사용되는 class_klass 도 볼 수 있음).
  • 함수 내부에 클래스를 만들었습니다. 이 함수를 호출할 때마다 클래스가 생성됩니다.
  • 해당 클래스를 함수의 반환 값으로 반환했습니다.

또한 완전히 선택 사항이지만 디버깅에 도움이 될 수 있는 LoggingThing.__name__ 을 설정합니다.

이 기술의 또 다른 적용은 우리가 때때로 클래스에 추가하고 싶은 모든 기능이 있고 이러한 기능의 다양한 조합을 추가하고 싶을 때입니다. 필요한 모든 다양한 조합을 수동으로 생성하는 것은 매우 다루기 어려울 수 있습니다.

런타임이 아닌 컴파일 시간에 클래스가 생성되는 언어에서는 이것이 불가능합니다. 대신 데코레이터 패턴을 사용해야 합니다. 이 패턴은 때때로 Python에서 유용할 수 있지만 대부분은 위의 기술을 사용할 수 있습니다.

일반적으로 저는 실제로 커스터마이징을 위해 많은 서브클래스를 만드는 것을 피합니다. 일반적으로 클래스를 전혀 포함하지 않는 더 간단하고 Python적인 방법이 있습니다. 그러나 이 기술은 필요한 경우 사용할 수 있습니다. Python의 데코레이터 패턴에 대한 Brandon Rhodes의 전체 처리도 참조하세요.

예외로서의 클래스

클래스가 사용되는 또 다른 위치는 try/except/finally 문의 except 절입니다. 이러한 클래스도 매개변수화할 수 있다고 생각하는 것은 놀라운 일이 아닙니다.

예를 들어 다음 코드는 실패할 수 있는 작업을 시도하고 최대 시도 횟수에 도달할 때까지 지수 백오프로 재시도하는 매우 일반적인 전략을 구현합니다.

 import time def retry_with_backoff(action, exceptions_to_catch, max_attempts=10, attempts_so_far=0): try: return action() except exceptions_to_catch: attempts_so_far += 1 if attempts_so_far >= max_attempts: raise else: time.sleep(attempts_so_far ** 2) return retry_with_backoff(action, exceptions_to_catch, attempts_so_far=attempts_so_far, max_attempts=max_attempts)

취할 조치와 catch할 예외를 매개변수로 모두 꺼냈습니다. exceptions_to_catch 매개변수는 IOError 또는 httplib.client.HTTPConnectionError 와 같은 단일 클래스 또는 이러한 클래스의 튜플일 수 있습니다. (다른 프로그래밍 오류를 숨기는 것으로 알려져 있기 때문에 "bare except" 절이나 except Exception 를 피하고 싶습니다.)

경고 및 결론

매개변수화는 코드를 재사용하고 코드 중복을 줄이기 위한 강력한 기술입니다. 몇 가지 단점이 없는 것은 아닙니다. 코드 재사용을 추구하는 과정에서 다음과 같은 몇 가지 문제가 종종 나타납니다.

  • 이해하기 매우 어렵게 되는 지나치게 일반적이거나 추상화된 코드.
  • 실제로는 특정 매개변수 조합만 적절하게 테스트되기 때문에 큰 그림을 모호하게 하거나 버그를 도입하는 매개변수가 급증하는 코드입니다.
  • "공통 코드"가 한 곳에서 제외되었기 때문에 코드베이스의 서로 다른 부분을 결합하는 데 도움이 되지 않습니다. 때때로 두 위치의 코드는 우연히 유사하며 두 위치는 독립적으로 변경해야 할 수 있으므로 서로 독립적이어야 합니다.

때때로 약간의 "중복된" 코드가 이러한 문제보다 훨씬 나으므로 이 기술을 주의해서 사용하십시오.

이 포스트에서 우리는 의존성 주입 , 전략 , 템플릿 메소드 , 추상 팩토리 , 팩토리 메소드데코레이터 로 알려진 디자인 패턴을 다루었습니다. Python에서 이들 중 다수는 실제로 매개변수화의 간단한 응용 프로그램으로 판명되거나 Python의 매개변수가 호출 가능한 객체 또는 클래스가 될 수 있다는 사실 때문에 확실히 불필요합니다. 바라건대 이것은 "실제 Python 개발자로서 알아야 할 사항"의 개념적 부담을 줄이고 간결한 Pythonic 코드를 작성할 수 있게 해줍니다!

추가 읽기:

  • Python 디자인 패턴: 세련되고 세련된 코드를 위해
  • Python 패턴: Python 디자인 패턴용
  • Python 로깅: 심층 자습서