요즘 대부분의 응용 프로그램에는 하나가 있습니다. 취소 작업. 상상하기 어렵지만 수년 동안 어떤 소프트웨어에도 실행 취소 기능이 없었습니다. 실행 취소는 1974년에 도입되었습니다(j.mp/wiundo), 하지만 여전히 널리 사용되는 프로그래밍 언어인 Fortran과 Lisp는 각각 1957년과 1958년에 만들어졌습니다.j.mp/proghist)! 나 응용 프로그램이 아니었을 것입니다
그 해의 사용자. 실수를 한다는 것은 사용자가 쉽게 고칠 방법이 없다는 것을 의미했습니다.
이야기로 충분합니다. 우리는 응용 프로그램에서 실행 취소 기능을 구현하는 방법을 알고 싶습니다. 그리고 이 장의 제목을 읽었으므로 실행 취소를 구현하기 위해 권장되는 디자인 패턴인 명령 패턴을 이미 알고 있습니다.
명령 디자인 패턴은 작업(실행 취소, 다시 실행, 복사, 붙여넣기 등)을 개체로 캡슐화하는 데 도움이 됩니다. 이는 단순히 작업을 구현하는 데 필요한 모든 논리와 메서드를 포함하는 클래스를 생성한다는 의미입니다. 장점은 다음과 같습니다(j.mp/cmdpattern):
- 명령을 직접 실행할 필요가 없습니다. 마음대로 실행할 수 있습니다.
- 명령을 호출하는 개체는 실행 방법을 알고 있는 개체에서 분리됩니다. 호출자는 명령에 대한 구현 세부 정보를 알 필요가 없습니다.
- 적절한 경우 호출자가 순서대로 실행할 수 있도록 여러 명령을 함께 그룹화할 수 있습니다. 예를 들어 이는 다중 수준 실행 취소 명령을 구현할 때 유용합니다.
- 명령을 호출하는 개체는 실행 방법을 알고 있는 개체에서 분리됩니다. 호출자는 명령에 대한 구현 세부 정보를 알 필요가 없습니다.
이 장에서는 다음에 대해 논의합니다.
- 실용적인 예
- 사용 사례
- 구현
- 사용 사례
우리는 식당에 저녁을 먹으러 갈 때 웨이터에게 주문을 합니다. 그들이 주문을 적는 수표(보통 종이)는 주문의 한 예입니다. 주문을 작성한 후 웨이터는 요리사가 수행하는 확인 대기열에 주문을 넣습니다. 각 검사는 독립적이며 쿠킹할 각 항목에 대한 하나의 명령과 같이 다양한 명령을 실행하는 데 사용할 수 있습니다.
예상하셨겠지만 몇 가지 소프트웨어 샘플도 있습니다. 두 가지가 떠오른다:
- PyQt는 QT 툴킷의 Python 바인딩입니다. PyQt에는 작업을 명령으로 모델링하는 QAction 클래스가 포함되어 있습니다. 다음과 같은 추가 선택적 정보가 각 작업에 대해 지원됩니다. B. 설명, 툴팁, 링크 등(j.mp/qaction).
- 힘내 콜라 (j.mp/git-cola), 자식 Python으로 작성된 GUI는 다음 명령을 사용합니다.
모델 변경, 커밋 변경, 다른 선택 적용, 체크아웃 등의 패턴(j.mp/git-cola-code).
많은 개발자는 실행 취소 예제를 명령 패턴의 유일한 사용 사례로 사용합니다. 진실은 실행 취소가 명령 패턴의 킬러 기능이라는 것입니다. 그러나 명령 패턴은 실제로 훨씬 더 많은 작업을 수행할 수 있습니다(j.mp/commddp):
- GUI 버튼 및 메뉴 항목: 이미 언급한 PyQt 예제는 명령 패턴을 사용하여 버튼 및 메뉴 항목에 대한 작업을 구현합니다.
- 다른 작업: 실행 취소 외에도 명령을 사용하여 모든 작업을 수행할 수 있습니다. 몇 가지 예로 잘라내기, 복사, 붙여넣기, 반복 및 대문자 텍스트를 들 수 있습니다.
- 트랜잭션 동작 및 로깅: 트랜잭션 동작 및 로깅은 변경 사항에 대한 지속적인 로그를 유지하는 데 중요합니다. 운영 체제에서 충돌 복구, 관계형 데이터베이스에서 트랜잭션 구현, 파일 시스템에서 스냅샷 구현, 설치 프로그램(마법사)에서 중단된 설치에서 복구하는 데 사용됩니다.
- 매크로: 이 경우 매크로란 언제든지 기록되고 필요할 때 실행할 수 있는 일련의 작업을 의미합니다. Emacs 및 Vim과 같은 일반 편집기는 매크로를 지원합니다.
- 트랜잭션 동작 및 로깅: 트랜잭션 동작 및 로깅은 변경 사항에 대한 지속적인 로그를 유지하는 데 중요합니다. 운영 체제에서 충돌 복구, 관계형 데이터베이스에서 트랜잭션 구현, 파일 시스템에서 스냅샷 구현, 설치 프로그램(마법사)에서 중단된 설치에서 복구하는 데 사용됩니다.
- 다른 작업: 실행 취소 외에도 명령을 사용하여 모든 작업을 수행할 수 있습니다. 몇 가지 예로 잘라내기, 복사, 붙여넣기, 반복 및 대문자 텍스트를 들 수 있습니다.
이 섹션에서는 명령 패턴을 사용하여 가장 기본적인 파일 유틸리티를 구현합니다.
- 파일 생성 및 선택적으로 텍스트(문자열) 쓰기
- 파일 내용 읽기
- 파일 이름 바꾸기
- 파일 삭제
- 파일 이름 바꾸기
- 파일 내용 읽기
Python은 이미 os 모듈에서 이러한 유틸리티의 좋은 구현을 제공하므로 처음부터 이러한 유틸리티를 구현하지 않을 것입니다. 명령으로 처리할 수 있도록 추가 수준의 추상화를 추가하려고 합니다. 이러한 방식으로 우리는 명령이 제공하는 모든 이점을 얻습니다.
표시된 작업에서 파일 이름 바꾸기 및 파일 지원 만들기를 실행 취소할 수 있습니다. 파일 삭제 및 파일 내용 읽기는 실행 취소를 지원하지 않습니다. 실행 취소는 실제로 파일 삭제 작업에서 구현될 수 있습니다. 한 가지 기술은 삭제된 모든 파일을 저장하는 특수 휴지통/휴지통 디렉토리를 사용하여 사용자가 요청할 경우 복원할 수 있도록 하는 것입니다. 이것은 모든 최신 데스크톱 환경에서 사용되는 기본 동작이며 연습으로 남겨둡니다.

각 명령은 두 부분으로 구성됩니다.
초기화 부분: __init__() 메서드에 의해 수행되며
명령이 유용한 작업을 수행하는 데 필요한 모든 정보(파일의 경로, 파일에 기록될 내용 등)를 포함합니다.
- 실행 부분: execute() 메소드에 의해 수행됩니다. 우리는 실제로 명령을 실행하고 싶을 때 execute() 메서드를 호출합니다. 이것은 반드시 초기화 직후일 필요는 없습니다.
RenameFile 클래스를 사용하여 구현된 이름 바꾸기 유틸리티부터 시작하겠습니다. __init__() 메서드는 소스(src) 및 대상(dest) 파일 경로를 매개 변수(문자열)로 허용합니다. 경로 구분 기호를 사용하지 않으면 현재 디렉터리를 사용하여 파일을 만듭니다. 경로 구분 기호를 사용하는 예는 문자열 /tmp/file1을 src로 전달하고
문자열 /home/user/file2를 대상으로 합니다. 경로를 사용하지 않는 또 다른 예는 file1을 src로, file2를 dest로 전달하는 것입니다.
클래스 이름 바꾸기파일:
def __init__(self, source, target): self.src = 소스
그 자체. 목표 = 목표
클래스에 execute() 메서드를 추가합니다. 이 메서드는 os.rename()을 사용하여 실제 이름 바꾸기를 수행합니다. verbose 변수는 전역 변수에 해당합니다. 깃발, 활성화되면(기본적으로 활성화됨) 수행 중인 작업에 대한 사용자 피드백을 제공합니다. 자동 명령을 선호하는 경우 비활성화할 수 있습니다. print()가 예로 충분하지만 로깅 모듈(j.mp/py3log):
실행 def(자체):
장황한 경우:
print(f”(‘{self.src}’를 ‘{self.dest}’로 이름 바꾸기)”) os.rename(self.src, self.dest)
이름 바꾸기 유틸리티(RenameFile)는 Undo() 메서드를 통해 실행 취소 작업을 지원합니다. 이 경우 os.rename()을 다시 사용하여 파일 이름을 원래 값으로 재설정합니다.
데프 실행 취소(자체):
장황한 경우:
print(f”(‘{self.dest}’ 이름을 ‘{self.src}’로 다시 이름 바꾸기)”) os.rename(self.dest, self.src)
이 예제에서 파일 삭제는 클래스가 아닌 함수에서 구현됩니다. 이는 추가하려는 각 명령에 대해 새 클래스를 만드는 것이 필수가 아님을 보여주기 위한 것입니다(자세한 내용은 나중에 설명). delete_file() 함수는 파일 경로를 문자열로 받아들이고 os.remove()를 사용하여 삭제합니다:
def delete_file(경로):
장황한 경우:
print(f”{경로} 파일 삭제”) os.remove(경로)
클래스 사용으로 돌아갑니다. CreateFile 클래스는 파일을 만드는 데 사용됩니다. 이 클래스의 __init__() 메서드는 파일에 기록될 콘텐츠(문자열)에 대한 알려진 경로 매개 변수와 txt 매개 변수를 허용합니다. 아무 것도 txt로 전달되지 않으면 기본 텍스트 “Hello World”가 파일에 기록됩니다. 일반적으로 현명한 기본 동작은 빈 파일을 만드는 것이지만 이 예제의 필요에 따라 기본 문자열을 작성하도록 선택했습니다.
CreateFile 클래스의 정의는 다음과 같이 시작됩니다.
클래스 CreateFile:
def __init__(self, path, txt=’Hello World\n’): self.path = 경로
self.txt = txt
그런 다음 execute() 메서드를 추가합니다. 여기서 with 문과 Python의 내장 open() 함수를 사용하여 파일을 열고(mode=’w’는 쓰기 모드를 의미함) write() 함수를 사용하여 txt를 씁니다. 파일 문자열은 다음과 같습니다.
실행 def(자체):
장황한 경우:
print(f”(‘{self.path}’ 파일 생성)”)
open(self.path, mode=’w’, encoding=’utf-8′)을 out_file로 사용: out_file.write(self.txt)
파일 생성 작업의 실행 취소는 해당 파일을 삭제하는 것입니다. 우리가 클래스에 추가하는 undo() 메서드는 단순히 delete_file() 함수를 사용하여 다음과 같이 이를 달성합니다.
def undo(self): delete_file(self.path)
마지막 유틸리티는 파일의 내용을 읽을 수 있는 가능성을 제공합니다. ReadFile 클래스의 execute() 메서드는 이번에는 읽기 모드에서 open()을 다시 사용하고 단순히 print()로 파일 내용을 인쇄합니다.
ReadFile 클래스는 다음과 같이 정의됩니다.
클래스 읽기 파일:
def __init__(self, path): self.path = 경로
실행 def(자체):
장황한 경우:
print(f”(‘{self.path}’ 파일 읽기)”)
with open(self.path, mode=’r’, encoding=’utf-8′) as in_file: print(in_file.read(), end=”)
main() 함수는 우리가 정의한 유틸리티를 사용합니다. orig_name 및 new_name 매개변수는 생성되고 이름이 바뀌는 파일의 원래 이름과 새 이름입니다. 명령 목록은 나중에 실행하려는 명령을 추가(및 구성)하는 데 사용됩니다. 각 명령에 대해 명시적으로 execute()를 호출하지 않으면 명령이 실행되지 않습니다.
메인() 정의:
orig_name, new_name = ‘file1’, ‘file2’ 명령 = ( CreateFile(orig_name), ReadFile(orig_name),
파일 이름 바꾸기(original_name, new_name) )
(명령의 c에 대한 c.execute())
다음 단계는 실행된 명령을 실행 취소할지 여부를 사용자에게 묻는 것입니다. 사용자는 명령 실행 취소 여부를 선택합니다. 실행 취소를 선택하면 명령 목록의 모든 명령에 대해 Undo()가 수행됩니다. 그러나 모든 명령이 실행 취소를 지원하는 것은 아니므로 Undo() 메서드가 없을 때 생성되는 AttributeError 예외를 포착(및 무시)하는 데 예외 처리가 사용됩니다. 코드
다음과 같이 보일 것입니다:
answer = input(‘실행된 명령을 취소하시겠습니까? (y/n) ‘) if answer not in ‘yY’:
print(f”결과는 {new_name}입니다”)
출구()
역순으로 c에 대해(명령):
시도:
씨. 취소 ()
AttributeError를 전자로 제외:
print(“오류”, str(e))
이러한 경우에 예외 처리를 사용하는 것은 허용되는 방법이지만, 마음에 들지 않으면 다음을 추가하여 명령이 실행 취소 작업을 지원하는지 명시적으로 확인할 수 있습니다. 부울 메서드(예: supports_undo() 또는 can_be_undone()). 이것도 필수는 아닙니다.
다음은 예제의 전체 코드입니다(command.py).
- os 모듈을 가져오고 필요한 상수를 정의합니다.
가져오기 OS 상세 정보 = True
- 여기에서 파일 이름 바꾸기 작업에 대한 클래스를 정의합니다.
클래스 이름 바꾸기파일:
def __init__(자신, 소스, 대상):
self.src = 소스
그 자체. 목표 = 목표
실행 def(자체):
장황한 경우:
print(f”(‘{self.src}’를 ‘{self.dest}’로 이름 바꾸기)”) os.rename(self.src, self.dest)
데프 실행 취소(자체):
장황한 경우:
print(f”(‘{self.dest}’ 이름을 ‘{self.src}’로 다시 이름 바꾸기)”) os.rename(self.dest, self.src)
- 여기서 파일 생성 작업을 위한 클래스를 정의합니다.
클래스 CreateFile:
def __init__(self, path, txt=’Hello World\n’): self.path = 경로
self.txt = txt
실행 def(자체):
장황한 경우:
print(f”(‘{self.path}’ 파일 생성)”)
open(self.path, mode=’w’, encoding=’utf-8′)을 out_file로 사용:
out_file.write(self.txt)
데프 실행 취소(자체):
delete_file(self.경로)
- 또한 다음과 같이 파일 읽기 작업에 대한 클래스를 정의합니다.
클래스 읽기파일:
def __init__(self, path): self.path = 경로
실행 def(자체):
장황한 경우:
print(f”(‘{self.path}’ 파일 읽기)”)
open(self.path, mode=’r’, encoding=’utf-8′)을 in_file로 사용:
print(in_file.read(), end=”)
- 그리고 파일 삭제 작업을 위해 다음과 같이 (클래스가 아닌) 함수를 사용하기로 결정했습니다.
def delete_file(경로):
장황한 경우:
print(f”{경로} 파일 삭제”) os.remove(경로)
- 이제 프로그램의 주요 부분은 다음과 같습니다.
메인() 정의:
original_name, new_name = ‘파일1’, ‘파일2’
명령 = (
CreateFile(orig_name),
파일 읽기(orig_name),
파일 이름 바꾸기(original_name, new_name)
)
(명령의 c에 대한 c.execute())
answer = input(‘실행된 명령을 취소하시겠습니까? (y/n) ‘) if answer not in ‘yY’:
print(f”결과는 {new_name}입니다”)
출구()
역순으로 c에 대해(명령):
시도:
씨. 취소 ()
AttributeError를 전자로 제외:
print(“오류”, str(e))
__name__ == “__main__”인 경우:
주로()
python command.py 명령줄을 사용한 두 가지 샘플 실행을 살펴보겠습니다. 처음에는 명령을 취소할 수 없습니다.

두 번째에는 실행 취소 명령이 있습니다.

하지만 기다려! 명령 구현 예제에서 개선할 수 있는 사항을 살펴보겠습니다. 무엇보다도 다음 사항을 준수해야 합니다.
- 존재하지 않는 파일의 이름을 바꾸려고 하면 어떻게 됩니까?
- 존재하지만 올바른 파일 시스템 권한이 없기 때문에 이름을 바꿀 수 없는 파일은 어떻게 됩니까?
일종의 오류 처리를 수행하여 유틸리티를 개선할 수 있습니다. os 모듈에서 함수의 반환 상태를 확인하는 것이 도움이 될 수 있습니다. os.path.exists() 함수를 사용하여 삭제 작업을 시도하기 전에 파일이 존재하는지 확인할 수 있습니다.
또한 파일 생성 유틸리티는 파일 시스템에서 설정한 기본 파일 권한을 사용하여 파일을 생성합니다. 예를 들어 POSIX 시스템에서 권한은 -rw-rw-r–입니다. 너
적절한 매개 변수를 CreateFile에 전달하여 사용자가 자신의 권한을 제공하도록 허용할 수 있습니다. 어떻게 했니? 힌트: 한 가지 가능성은 os.fdopen()을 사용하는 것입니다.
그리고 이제 여기서 생각해 볼 것이 있습니다. 명령이 반드시 클래스일 필요는 없다고 이미 언급했습니다. 이것이 삭제 유틸리티가 구현된 방식입니다. delete_file() 함수는 하나뿐입니다. 그것의 장단점은 무엇입니까
접근하다? 힌트: 나머지 명령에 대해 수행된 것처럼 명령 목록에 삭제 명령을 포함할 수 있습니까? 우리는 함수가 Python의 일급 시민이라는 것을 알고 있으므로 다음과 같이 할 수 있습니다(first-class.py 파일 참조).
가져오기 OS 상세 정보 = True
클래스 CreateFile:
def __init__(self, path, txt=’Hello World\n’): self.path = 경로
self.txt = txt
실행 def(자체):
장황한 경우:
print(f”(‘{self.path}’ 파일 생성)”)
open(self.path, mode=’w’, encoding=’utf-8′)을 out_file로 사용: out_file.write(self.txt)
데프 실행 취소(자체):
시도:
delete_file(self.경로)
제외하고:
print(‘삭제 작업 실패…’)
print(‘… 파일이 이미 삭제되었을 수 있습니다.’)
def delete_file(경로):
장황한 경우:
print(f”{경로} 파일 삭제 중…”) os.remove(경로)
메인() 정의:
orig_name = ‘file1’ df=delete_file
명령 = (CreateFile(orig_name),) 명령.추가(df)
명령에서 c의 경우:
시도:
c.실행()
AttributeError를 e: df(orig_name)로 제외
역순으로 c에 대해(명령): 다음을 시도하십시오.
씨. 취소 ()
AttributeError를 전자로 제외: 패스
__name__ == “__main__”인 경우:
주로()
구현 예의 이 변형이 작동하지만 여전히 몇 가지 문제가 있습니다.
- 코드가 일정하지 않습니다. 우리는 프로그램의 정상적인 흐름이 아닌 예외 처리에 너무 많이 의존합니다. 구현한 다른 모든 명령에는 execute() 메서드가 있지만 이 경우에는 execute()가 없습니다.
- 현재 파일 삭제 유틸리티는 실행 취소를 지원하지 않습니다. 최종적으로 실행 취소 지원을 추가하기로 결정하면 어떻게 될까요? 일반적으로 명령을 나타내는 클래스에 Undo() 메서드를 추가합니다. 그러나 이 경우에는 클래스가 없습니다. 실행 취소를 수행하는 다른 함수를 만들 수도 있지만 클래스를 만드는 것이 더 나은 방법입니다.
이 장에서는 명령 패턴을 다루었습니다. 이 디자인 패턴을 사용하면 복사/붙여넣기와 같은 작업을 개체로 캡슐화할 수 있습니다. 이는 다음과 같은 많은 이점을 제공합니다.
- 우리는 원할 때마다 명령을 실행할 수 있으며 반드시 빌드 시간에 실행할 필요는 없습니다.
- 명령을 실행하는 클라이언트 코드는 구현 방법에 대한 세부 정보를 알 필요가 없습니다.
- 명령을 그룹화하고 특정 순서로 실행할 수 있습니다.
- 명령을 실행하는 클라이언트 코드는 구현 방법에 대한 세부 정보를 알 필요가 없습니다.
주문을 실행하는 것은 식당에서 주문하는 것과 같습니다. 각 고객의 주문은 여러 단계를 거쳐 궁극적으로 셰프가 실행하는 별도의 명령입니다.
PyQt를 비롯한 많은 GUI 프레임워크는 명령 패턴을 사용하여 하나 이상의 이벤트에 의해 트리거되고 사용자 정의될 수 있는 작업을 모델링합니다. 그러나 Command는 프레임워크에만 국한되지 않습니다. Git-Cola와 같은 일반 애플리케이션도 제공하는 이점 때문에 이를 사용합니다.
지금까지 가장 많이 광고된 명령 기능은 실행 취소이지만 더 많은 용도가 있습니다. 일반적으로 사용자의 재량에 따라 런타임에 수행할 수 있는 모든 작업은 명령 패턴을 사용하기에 좋은 후보입니다. 명령 패턴은 여러 명령을 함께 그룹화하는 데에도 유용합니다. 매크로, 다중 수준 실행 취소 및 트랜잭션을 구현하는 데 유용합니다. 트랜잭션은 성공해야 합니다. 즉, 모든 작업이 성공해야 하거나(커밋 작업), 작업 중 하나 이상이 실패하면 완전히 실패해야 합니다(롤백 작업). 명령 패턴을 다음 수준으로 가져가려면 명령이 트랜잭션으로 그룹화되는 예제에서 작업할 수 있습니다.
명령을 시연하기 위해 Python의 os 모듈 위에 몇 가지 기본 파일 유틸리티를 구현했습니다. 우리의 유틸리티는 실행 취소를 지원하고 명령을 쉽게 그룹화할 수 있는 통합 인터페이스를 가졌습니다.
다음 장에서는 관찰자 패턴을 다룹니다.
(122)
)
- 하나

*