การตรวจสอบโค้ดที่สะอาด: ดูที่ Python, Parameterized
เผยแพร่แล้ว: 2022-03-11ในโพสต์นี้ ฉันจะพูดถึงสิ่งที่ฉันคิดว่าเป็นเทคนิคหรือรูปแบบที่สำคัญที่สุดในการสร้างโค้ด Pythonic ที่สะอาดหมดจด กล่าวคือ การกำหนดพารามิเตอร์ โพสต์นี้เหมาะสำหรับคุณหาก:
- คุณค่อนข้างใหม่กับรูปแบบการออกแบบทั้งหมดและอาจสับสนเล็กน้อยกับชื่อรูปแบบและไดอะแกรมคลาสยาว ข่าวดีก็คือมีรูปแบบการออกแบบเพียงรูปแบบเดียวเท่านั้นที่คุณต้องรู้สำหรับ Python ยิ่งไปกว่านั้น คุณอาจจะรู้อยู่แล้ว แต่อาจไม่ใช่ทุกวิธีที่สามารถนำมาใช้ได้
- คุณมาที่ Python จากภาษา OOP อื่น เช่น Java หรือ C# และต้องการทราบวิธีการแปลความรู้ของคุณเกี่ยวกับรูปแบบการออกแบบจากภาษานั้นไปยัง Python ใน Python และภาษาที่พิมพ์แบบไดนามิกอื่นๆ รูปแบบต่างๆ ที่พบบ่อยในภาษา OOP ที่พิมพ์แบบสแตติกนั้น “มองไม่เห็นหรือง่ายกว่า” ตามที่ผู้เขียน Peter Norvig กล่าวไว้
ในบทความนี้ เราจะมาสำรวจการใช้ "การกำหนดพารามิเตอร์" และวิธีการที่เกี่ยวข้องกับรูปแบบการออกแบบกระแสหลักที่เรียกว่า การพึ่งพาอาศัยกัน , กลยุทธ์ , วิธีเทมเพลต , โรงงานนามธรรม , วิธีโรงงาน และ มัณฑนากร ใน Python สิ่งเหล่านี้หลายๆ อย่างกลายเป็นความเรียบง่ายหรือไม่จำเป็นเนื่องจากข้อเท็จจริงที่ว่าพารามิเตอร์ใน Python สามารถเป็นอ็อบเจกต์หรือคลาสที่เรียกได้
การกำหนดพารามิเตอร์เป็นกระบวนการในการรับค่าหรืออ็อบเจ็กต์ที่กำหนดไว้ในฟังก์ชันหรือเมธอด และทำให้เป็นพารามิเตอร์สำหรับฟังก์ชันหรือเมธอดนั้น เพื่อที่จะสรุปโค้ด กระบวนการนี้เรียกอีกอย่างว่าการปรับโครงสร้าง "พารามิเตอร์แยก" บทความนี้เป็นเรื่องเกี่ยวกับรูปแบบการออกแบบและการปรับโครงสร้างใหม่
กรณีที่ง่ายที่สุดของ Python Parameterized
สำหรับตัวอย่างส่วนใหญ่ เราจะใช้โมดูลเต่าไลบรารีมาตรฐานการสอนสำหรับการทำกราฟิก
นี่คือรหัสบางส่วนที่จะวาดสี่เหลี่ยมขนาด 100x100 โดยใช้ turtle
:
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_square
สร้างอินสแตนซ์ Turtle
เอง (ซึ่งฉันจะพูดถึงในภายหลัง) สิ่งนี้อาจไม่เป็นที่ต้องการหากเราต้องการใช้ Turtle
ตัวเดียวสำหรับการวาดภาพทั้งหมดของเรา สำหรับตอนนี้ เราจะใช้การกำหนดพารามิเตอร์อีกครั้งเพื่อให้ turtle
เป็นพารามิเตอร์ draw_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_square
ต้องการ Turtle
ผู้เรียกมีหน้าที่ส่งผ่านวัตถุนั้นเป็นพารามิเตอร์ ไม่ จริงๆ หากคุณเคยสงสัยเกี่ยวกับการฉีดพึ่งพา Python นี่แหละ
จนถึงตอนนี้ เราได้จัดการกับการใช้งานพื้นฐานสองแบบ ข้อสังเกตที่สำคัญสำหรับส่วนที่เหลือของบทความนี้คือ ใน Python มีสิ่งต่างๆ มากมายที่สามารถกลายเป็นพารามิเตอร์ได้ มากกว่าในภาษาอื่นบางภาษา และทำให้เป็นเทคนิคที่ทรงพลังมาก
อะไรก็ได้ที่เป็นวัตถุ
ใน Python คุณสามารถใช้เทคนิคนี้เพื่อสร้างพารามิเตอร์อะไรก็ได้ที่เป็นอ็อบเจกต์ และใน Python สิ่งที่คุณเจอส่วนใหญ่เป็นอ็อบเจกต์ ซึ่งรวมถึง:
- อินสแตนซ์ของประเภทในตัว เช่น สตริง
"I'm a string"
และจำนวนเต็ม42
หรือพจนานุกรม - อินสแตนซ์ของประเภทและคลาสอื่นๆ เช่น
datetime.datetime
object - หน้าที่และวิธีการ
- ชนิดในตัวและคลาสที่กำหนดเอง
สองข้อสุดท้ายเป็นสิ่งที่น่าประหลาดใจที่สุด โดยเฉพาะอย่างยิ่งถ้าคุณมาจากภาษาอื่น และพวกเขาต้องการการอภิปรายเพิ่มเติม
ทำหน้าที่เป็นพารามิเตอร์
คำสั่งฟังก์ชันใน Python ทำสองสิ่ง:
- มันสร้างวัตถุฟังก์ชั่น
- มันสร้างชื่อในขอบเขตท้องถิ่นที่ชี้ไปที่วัตถุนั้น
เราสามารถเล่นกับวัตถุเหล่านี้ใน 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()
แต่บางทีเราก็ไม่อยากหยุด วิธีที่ง่ายที่สุดในการทำเช่นนี้คือการเพิ่มพารามิเตอร์ pause
อาจมีค่าเริ่มต้นเป็นศูนย์ เพื่อที่โดยค่าเริ่มต้นเราจะไม่หยุดชั่วคราว
อย่างไรก็ตาม เราค้นพบในภายหลังว่าบางครั้งเราต้องการทำสิ่งที่แตกต่างไปจากเดิมอย่างสิ้นเชิงที่มุม บางทีเราต้องการวาดรูปทรงอื่นในแต่ละมุม เปลี่ยนสีปากกา ฯลฯ เราอาจถูกล่อลวงให้เพิ่มพารามิเตอร์อีกมากมาย อย่างใดอย่างหนึ่งสำหรับแต่ละสิ่งที่เราต้องทำ อย่างไรก็ตาม วิธีแก้ปัญหาที่ดีกว่ามากคือยอมให้ส่งผ่านฟังก์ชันใดๆ ไปในการดำเนินการ สำหรับค่าเริ่มต้น เราจะสร้างฟังก์ชันที่ไม่ทำอะไรเลย นอกจากนี้เรายังจะทำให้ฟังก์ชันนี้ยอมรับพารามิเตอร์ท้องถิ่นของ turtle
และ size
ในกรณีที่จำเป็น:
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 ทำให้สิ่งนี้ง่ายมาก ในภาษาที่ไม่มีหรือบางภาษาที่พิมพ์แบบสแตติกซึ่งต้องใช้ลายเซ็นประเภทสำหรับพารามิเตอร์ อาจทำได้ยากกว่านี้ เราจะทำสิ่งนี้ได้อย่างไรถ้าเราไม่มีฟังก์ชันชั้นหนึ่ง
ทางออกหนึ่งคือเปลี่ยน draw_square
เป็นคลาส SquareDrawer
:
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
ที่ทำในสิ่งที่เราต้องการ รูปแบบหลามนี้เรียกว่ารูปแบบวิธีการเทมเพลต—คลาสพื้นฐานกำหนดรูปร่างของการดำเนินการทั้งหมดหรืออัลกอริธึม และส่วนต่าง ๆ ของการดำเนินการจะถูกใส่ลงในวิธีการที่จำเป็นต้องนำไปใช้โดยคลาสย่อย
แม้ว่าบางครั้งอาจมีประโยชน์ใน Python แต่การดึงรหัสตัวแปรออกมาในฟังก์ชันที่ส่งผ่านอย่างง่าย ๆ เนื่องจากพารามิเตอร์มักจะง่ายกว่ามาก
วิธีที่สองที่เราอาจแก้ปัญหานี้ในภาษาที่ไม่มีฟังก์ชันชั้นหนึ่งคือการรวมฟังก์ชันของเราเป็นเมธอดภายในคลาส เช่นนี้
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 โดยเฉพาะอย่างยิ่งหากคลาสกลยุทธ์มีชุดของฟังก์ชันที่เกี่ยวข้องจริง ๆ แทนที่จะเป็นเพียงชุดเดียว อย่างไรก็ตาม บ่อยครั้งที่เราต้องการเพียงฟังก์ชัน และเราสามารถหยุดเขียนชั้นเรียนได้
Callables อื่น ๆ
ในตัวอย่างข้างต้น ฉันได้พูดถึงการส่งฟังก์ชันไปยังฟังก์ชันอื่นเป็นพารามิเตอร์ อย่างไรก็ตาม แท้จริงแล้ว ทุกสิ่งที่ฉันเขียนนั้นเป็นจริงสำหรับอ็อบเจกต์ callable ใดๆ ฟังก์ชันเป็นตัวอย่างที่ง่ายที่สุด แต่เราสามารถพิจารณาเมธอดได้ด้วย
สมมติว่าเรามีรายการ foo
:
foo = [1, 2, 3]
ตอนนี้ foo
มีเมธอดมากมายแนบอยู่ เช่น .append()
และ .count()
“วิธีการผูกมัด” เหล่านี้สามารถส่งผ่านและใช้เหมือนกับฟังก์ชัน:
> >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]
นอกจากเมธอดของอินสแตนซ์เหล่านี้ ยังมีอ็อบเจ็กต์ที่เรียกได้ประเภทอื่นๆ— staticmethods
และ classmethods
, อินสแตนซ์ของคลาสที่ใช้ __call__
และคลาส/ประเภทเอง

คลาสเป็นพารามิเตอร์
ใน Python คลาสคือ "เฟิร์สคลาส"—เป็นอ็อบเจกต์รันไทม์เหมือนกับ dicts, strings ฯลฯ ซึ่งอาจดูแปลกกว่าฟังก์ชันที่เป็นอ็อบเจกต์ แต่โชคดีที่มันง่ายกว่าที่จะแสดงให้เห็นความจริงข้อนี้มากกว่าสำหรับฟังก์ชัน
คำสั่งคลาสที่คุณคุ้นเคยเป็นวิธีที่ดีในการสร้างคลาส แต่ไม่ใช่วิธีเดียว—เรายังสามารถใช้ประเภทอาร์กิวเมนต์สามเวอร์ชันได้ สองข้อความต่อไปนี้ทำสิ่งเดียวกันทุกประการ:
class Foo: pass Foo = type('Foo', (), {})
ในเวอร์ชันที่สอง ให้สังเกตสองสิ่งที่เราเพิ่งทำ (ซึ่งสะดวกกว่าโดยใช้คำสั่งของชั้นเรียน):
- ทางด้านขวามือของเครื่องหมายเท่ากับ เราได้สร้างคลาสใหม่ โดยใช้ชื่อภายในของ
Foo
นี่คือชื่อที่คุณจะได้กลับมาถ้าคุณทำFoo.__name__
- ด้วยการมอบหมายงาน เราจึงสร้างชื่อในขอบเขตปัจจุบัน 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
แต่ถ้าบางครั้งหน้าที่ของเราจำเป็นต้องสร้างเต่าหลายตัวเพื่อจุดประสงค์ในการวาดภาพที่แตกต่างกัน หรือบางทีมันอาจต้องการเริ่มสี่เส้น โดยแต่ละตัวมีเต่าของตัวเองเพื่อวาดด้านใดด้านหนึ่งของสี่เหลี่ยม คำตอบคือเพียงแค่ทำให้คลาส 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)
หรือเราสามารถ subclass Turtle
เพื่อให้พฤติกรรมนั้นสร้างขึ้นและส่งผ่าน subclass เป็นพารามิเตอร์:
class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)
ในภาษาอื่น…
ภาษา OOP อื่นๆ อีกหลายภาษา เช่น Java และ C# ไม่มีคลาสระดับเฟิร์สคลาส หากต้องการยกตัวอย่างคลาส คุณต้องใช้คีย์เวิร์ด new
ตามด้วยชื่อคลาสจริง
ข้อจำกัดนี้เป็นสาเหตุของรูปแบบอย่าง abstract factory (ซึ่งต้องมีการสร้างชุดของคลาสที่มีงานเดียวคือการสร้างอินสแตนซ์ของคลาสอื่นๆ) และรูปแบบ Factory Method อย่างที่คุณเห็นใน 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")
สิ่งเดียวที่แตกต่างกันระหว่างสองสิ่งนี้คือ:
- คลาสพื้นฐาน
- ชื่อของคลาสย่อย—แต่เราไม่ได้สนใจเรื่องนั้นจริงๆ และสามารถสร้างได้โดยอัตโนมัติจากแอตทริบิวต์คลาสฐาน
__name__
- ชื่อที่ใช้ในการโทร
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 แต่ส่วนใหญ่คุณสามารถใช้เทคนิคข้างต้นได้
โดยปกติ จริงๆ แล้ว ฉันหลีกเลี่ยงการสร้างคลาสย่อยจำนวนมากสำหรับการปรับแต่ง โดยปกติมีวิธี Pythonic ที่ง่ายกว่าและไม่เกี่ยวข้องกับคลาสเลย แต่เทคนิคนี้มีให้ถ้าคุณต้องการ ดูเพิ่มเติมที่การรักษารูปแบบมัณฑนากรเต็มรูปแบบของแบรนดอนโรดส์ใน Python
คลาสเป็นข้อยกเว้น
อีกที่หนึ่งที่คุณเห็นว่ามีการใช้คลาสอยู่ในส่วนคำสั่ง 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)
เราได้ดึงทั้งการกระทำที่ต้องทำและข้อยกเว้นที่จับมาเป็นพารามิเตอร์ พารามิเตอร์ exceptions_to_catch
สามารถเป็นคลาสเดียว เช่น httplib.client.HTTPConnectionError
IOError
ทูเพิลของคลาสดังกล่าว (เราต้องการหลีกเลี่ยงส่วนคำสั่ง "bare ยกเว้น" หรือแม้แต่ except Exception
เนื่องจากเป็นที่รู้กันว่าซ่อนข้อผิดพลาดในการเขียนโปรแกรมอื่น ๆ ไว้)
คำเตือนและข้อสรุป
การกำหนดพารามิเตอร์เป็นเทคนิคที่มีประสิทธิภาพสำหรับการนำโค้ดกลับมาใช้ซ้ำและลดการซ้ำซ้อนของโค้ด มันไม่ได้โดยไม่มีข้อบกพร่องบางอย่าง ในการแสวงหาการใช้รหัสซ้ำ มักเกิดปัญหาหลายประการ:
- โค้ดทั่วไปหรือโค้ดที่เป็นนามธรรมมากเกินไปซึ่งยากต่อการทำความเข้าใจ
- โค้ดที่มีการเพิ่มจำนวนพารามิเตอร์ที่บดบังภาพใหญ่หรือทำให้เกิดข้อบกพร่อง เนื่องจากในความเป็นจริง มีเพียงชุดค่าผสมของพารามิเตอร์บางตัวเท่านั้นที่ได้รับการทดสอบอย่างเหมาะสม
- การรวมส่วนต่างๆ ของ codebase ที่ไม่มีประโยชน์เนื่องจาก "รหัสทั่วไป" ของพวกเขาถูกแยกออกเป็นที่เดียว บางครั้งโค้ดในสองแห่งอาจคล้ายกันโดยไม่ได้ตั้งใจ และทั้งสองแห่งควรแยกจากกันเนื่องจากอาจต้องเปลี่ยนโดยอิสระ
บางครั้งโค้ดที่ "ซ้ำซ้อน" เล็กน้อยก็ดีกว่าปัญหาเหล่านี้มาก ดังนั้นให้ใช้เทคนิคนี้อย่างระมัดระวัง
ในโพสต์นี้ เราได้กล่าวถึงรูปแบบการออกแบบที่เรียกว่า การเติมพึ่งพา , กลยุทธ์ , วิธีเทมเพลต , โรงงานนามธรรม , วิธีโรงงาน , และ มัณฑนากร ใน Python สิ่งเหล่านี้หลายๆ อย่างกลายเป็นแอปพลิเคชั่นง่ายๆ ของการกำหนดพารามิเตอร์หรือไม่จำเป็นอย่างแน่นอนเนื่องจากข้อเท็จจริงที่ว่าพารามิเตอร์ใน Python สามารถเรียกอ็อบเจกต์หรือคลาสได้ หวังว่านี่จะช่วยแบ่งเบาภาระแนวคิดของ "สิ่งที่คุณควรรู้ในฐานะนักพัฒนา Python ตัวจริง" และช่วยให้คุณสามารถเขียนโค้ด Pythonic ที่กระชับได้!
อ่านเพิ่มเติม:
- รูปแบบการออกแบบหลาม: สำหรับโค้ดที่เก๋ไก๋และทันสมัย
- รูปแบบหลาม: สำหรับรูปแบบการออกแบบหลาม
- การบันทึก Python: บทช่วยสอนเชิงลึก