“ ความประหลาดใจน้อยที่สุด” และอาร์กิวเมนต์เริ่มต้นที่เปลี่ยนแปลงไม่ได้
ใครก็ตามที่ใช้ Python นานพอจะถูกกัด (หรือฉีกเป็นชิ้น ๆ ) โดยปัญหาต่อไปนี้:
def foo(a=[]):
a.append(5)
return a
สามเณร Python คาดหวังว่าฟังก์ชันนี้จะส่งคืนรายการที่มีองค์ประกอบเดียว[5]
เสมอ: ผลลัพธ์ที่ได้กลับแตกต่างกันมากและน่าประหลาดใจมาก (สำหรับมือใหม่):
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
ผู้จัดการของฉันเคยพบครั้งแรกกับคุณลักษณะนี้และเรียกมันว่า "ข้อบกพร่องในการออกแบบที่น่าทึ่ง" ของภาษา ฉันตอบว่าพฤติกรรมนั้นมีคำอธิบายที่อยู่เบื้องหลังและเป็นเรื่องที่น่างงงวยและคาดไม่ถึงหากคุณไม่เข้าใจภายใน อย่างไรก็ตามฉันไม่สามารถตอบ (กับตัวเอง) คำถามต่อไปนี้: อะไรคือเหตุผลในการผูกอาร์กิวเมนต์เริ่มต้นที่นิยามฟังก์ชันและไม่ได้อยู่ที่การเรียกใช้ฟังก์ชัน ฉันสงสัยว่าพฤติกรรมที่มีประสบการณ์มีการใช้งานจริง (ใครใช้ตัวแปรคงที่ใน C โดยไม่มีข้อบกพร่องในการผสมพันธุ์?)
แก้ไข :
Baczek เป็นตัวอย่างที่น่าสนใจ เมื่อรวมกับความคิดเห็นส่วนใหญ่ของคุณและโดยเฉพาะอย่างยิ่ง Utaal ฉันได้อธิบายเพิ่มเติม:
>>> def a():
... print("a executed")
... return []
...
>>>
>>> def b(x=a()):
... x.append(5)
... print(x)
...
a executed
>>> b()
[5]
>>> b()
[5, 5]
สำหรับฉันแล้วดูเหมือนว่าการตัดสินใจในการออกแบบจะสัมพันธ์กับตำแหน่งที่จะวางขอบเขตของพารามิเตอร์: ภายในฟังก์ชันหรือ "ร่วมกัน" ด้วย?
การเชื่อมโยงภายในฟังก์ชันจะหมายความว่าx
ถูกผูกไว้อย่างมีประสิทธิภาพกับค่าเริ่มต้นที่ระบุเมื่อฟังก์ชันถูกเรียกใช้ไม่ได้กำหนดสิ่งที่จะนำเสนอข้อบกพร่องที่ลึกซึ้ง: def
บรรทัดจะเป็น "ไฮบริด" ในแง่ที่เป็นส่วนหนึ่งของการเชื่อมโยง (ของ วัตถุฟังก์ชัน) จะเกิดขึ้นที่นิยามและบางส่วน (การกำหนดพารามิเตอร์เริ่มต้น) ในเวลาเรียกใช้ฟังก์ชัน
พฤติกรรมที่แท้จริงมีความสอดคล้องกันมากขึ้น: ทุกอย่างของบรรทัดนั้นจะได้รับการประเมินเมื่อมีการดำเนินการบรรทัดนั้นซึ่งหมายถึงนิยามฟังก์ชัน
คำตอบ
อันที่จริงนี่ไม่ใช่ข้อบกพร่องในการออกแบบและไม่ใช่เพราะภายในหรือประสิทธิภาพ
มันมาจากข้อเท็จจริงที่ว่าฟังก์ชันใน Python เป็นออบเจ็กต์ชั้นหนึ่งไม่ใช่แค่ส่วนหนึ่งของโค้ดเท่านั้น
ทันทีที่คุณคิดในทางนี้มันก็สมเหตุสมผลอย่างยิ่ง: ฟังก์ชันคือวัตถุที่ถูกประเมินจากนิยามของมัน พารามิเตอร์เริ่มต้นเป็นชนิดของ "ข้อมูลสมาชิก" ดังนั้นสถานะของพวกเขาอาจเปลี่ยนจากการเรียกหนึ่งไปยังอีกรายการหนึ่ง - เหมือนกับในออบเจ็กต์อื่น ๆ
ในกรณีใด ๆ Effbot มีคำอธิบายที่ดีมากในเหตุผลสำหรับพฤติกรรมนี้ในการเริ่มต้นค่าพารามิเตอร์ในหลาม
ฉันพบว่ามันชัดเจนมากและฉันขอแนะนำให้อ่านเพื่อความรู้ที่ดีขึ้นเกี่ยวกับการทำงานของวัตถุฟังก์ชัน
สมมติว่าคุณมีรหัสต่อไปนี้
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
เมื่อฉันเห็นการประกาศกินสิ่งที่น่าประหลาดใจอย่างน้อยที่สุดคือคิดว่าถ้าไม่กำหนดพารามิเตอร์ตัวแรกมันจะเท่ากับทูเพิล ("apples", "bananas", "loganberries")
อย่างไรก็ตามในภายหลังในโค้ดฉันจะทำสิ่งที่ชอบ
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
ถ้าพารามิเตอร์ดีฟอลต์ถูกผูกไว้ที่การเรียกใช้ฟังก์ชันมากกว่าการประกาศฟังก์ชันฉันจะต้องประหลาดใจ (ในทางที่แย่มาก) เมื่อพบว่าผลไม้มีการเปลี่ยนแปลง นี่จะเป็น IMO ที่น่าประหลาดใจยิ่งกว่าการพบว่าfoo
ฟังก์ชันของคุณข้างต้นกำลังกลายพันธุ์รายการ
ปัญหาที่แท้จริงอยู่ที่ตัวแปรที่ไม่แน่นอนและทุกภาษาก็มีปัญหานี้ในระดับหนึ่ง นี่คือคำถาม: สมมติว่าใน Java ฉันมีรหัสต่อไปนี้:
StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) ); // does this work?
ตอนนี้แผนที่ของฉันใช้ค่าของStringBuffer
คีย์เมื่อวางลงในแผนที่หรือไม่หรือจัดเก็บคีย์โดยการอ้างอิง? ไม่ว่าจะด้วยวิธีใดมีคนประหลาดใจ ไม่ว่าจะเป็นผู้ที่พยายามดึงวัตถุออกจากการMap
ใช้ค่าที่เหมือนกับค่าที่ใส่ไว้หรือผู้ที่ดูเหมือนจะไม่สามารถดึงวัตถุของตนได้แม้ว่าคีย์ที่ใช้จะเป็นวัตถุเดียวกันก็ตาม ที่ใช้เพื่อใส่ลงในแผนที่ (นี่คือสาเหตุที่ Python ไม่อนุญาตให้ใช้ชนิดข้อมูลในตัวที่ไม่แน่นอนเป็นคีย์พจนานุกรม)
ตัวอย่างของคุณเป็นกรณีที่ดีที่ผู้มาใหม่ Python จะประหลาดใจและถูกกัด แต่ฉันจะเถียงว่าถ้าเรา "แก้ไข" สิ่งนี้สิ่งนี้ก็จะสร้างสถานการณ์ที่แตกต่างออกไปโดยที่พวกเขาจะถูกกัดแทนและสิ่งนั้นก็จะเข้าใจง่ายน้อยลง ยิ่งไปกว่านั้นกรณีนี้มักเกิดขึ้นเมื่อต้องจัดการกับตัวแปรที่ไม่แน่นอน คุณมักจะเจอกรณีที่มีคนคาดหวังพฤติกรรมอย่างใดอย่างหนึ่งหรือพฤติกรรมตรงกันข้ามโดยสังหรณ์ใจขึ้นอยู่กับว่าพวกเขากำลังเขียนโค้ดอะไร
ฉันชอบแนวทางปัจจุบันของ Python เป็นการส่วนตัว: อาร์กิวเมนต์ของฟังก์ชันเริ่มต้นจะได้รับการประเมินเมื่อกำหนดฟังก์ชันและวัตถุนั้นจะเป็นค่าเริ่มต้นเสมอ ฉันคิดว่าพวกเขาสามารถใช้กรณีพิเศษโดยใช้รายการว่าง แต่ปลอกพิเศษแบบนั้นจะทำให้เกิดความประหลาดใจมากยิ่งขึ้นไม่ต้องพูดถึงว่าจะเข้ากันไม่ได้
ส่วนที่เกี่ยวข้องของเอกสาร :
ค่าพารามิเตอร์เริ่มต้นจะถูกประเมินจากซ้ายไปขวาเมื่อเรียกใช้นิยามฟังก์ชัน ซึ่งหมายความว่านิพจน์จะได้รับการประเมินหนึ่งครั้งเมื่อกำหนดฟังก์ชันและจะใช้ค่า "คำนวณล่วงหน้า" เดียวกันสำหรับการโทรแต่ละครั้ง สิ่งนี้สำคัญอย่างยิ่งที่จะต้องทำความเข้าใจเมื่อพารามิเตอร์เริ่มต้นเป็นวัตถุที่ไม่แน่นอนเช่นรายการหรือพจนานุกรม: ถ้าฟังก์ชันแก้ไขวัตถุ (เช่นโดยการต่อท้ายรายการเข้ากับรายการ) ค่าเริ่มต้นจะมีผลแก้ไข โดยทั่วไปแล้วสิ่งนี้ไม่ใช่สิ่งที่ตั้งใจไว้ วิธีแก้ปัญหานี้คือการใช้
None
เป็นค่าเริ่มต้นและทดสอบอย่างชัดเจนในเนื้อหาของฟังก์ชันเช่น:def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of the zoo") return penguin
ฉันไม่รู้อะไรเลยเกี่ยวกับการทำงานภายในของล่าม Python (และฉันไม่ใช่ผู้เชี่ยวชาญด้านคอมไพเลอร์และล่ามด้วย) ดังนั้นอย่าโทษฉันถ้าฉันเสนออะไรที่ไม่สมเหตุสมผลหรือเป็นไปไม่ได้
หากวัตถุ python ไม่แน่นอนฉันคิดว่าสิ่งนี้ควรนำมาพิจารณาเมื่อออกแบบสิ่งที่อาร์กิวเมนต์เริ่มต้น เมื่อคุณสร้างอินสแตนซ์รายการ:
a = []
คุณคาดหวังที่จะได้รับใหม่a
รายการอ้างอิงโดย
ทำไมต้องa=[]
ใน
def x(a=[]):
สร้างอินสแตนซ์รายการใหม่ในนิยามฟังก์ชันไม่ใช่ในการเรียกใช้? เหมือนกับที่คุณกำลังถามว่า "หากผู้ใช้ไม่ได้ระบุอาร์กิวเมนต์ให้สร้างอินสแตนซ์รายการใหม่และใช้เหมือนกับว่าผู้โทรสร้างขึ้น" ฉันคิดว่าสิ่งนี้คลุมเครือแทน:
def x(a=datetime.datetime.now()):
ผู้ใช้คุณต้องการa
ตั้งค่าเริ่มต้นเป็นวันที่และเวลาที่สอดคล้องกับเวลาที่คุณกำหนดหรือดำเนินการx
หรือไม่? ในกรณีนี้เช่นเดียวกับก่อนหน้านี้ฉันจะคงพฤติกรรมเดิมไว้ราวกับว่าอาร์กิวเมนต์เริ่มต้น "การกำหนด" เป็นคำสั่งแรกของฟังก์ชัน ( datetime.now()
เรียกว่าการเรียกใช้ฟังก์ชัน) ในทางกลับกันหากผู้ใช้ต้องการการแม็ปกำหนดเวลาก็สามารถเขียนได้:
b = datetime.datetime.now()
def x(a=b):
ฉันรู้ฉันรู้ว่านั่นคือการปิด อีกทางเลือกหนึ่ง Python อาจให้คำสำคัญเพื่อบังคับให้มีการผูกเวลานิยาม:
def x(static a=b):
เหตุผลก็ค่อนข้างง่ายที่การผูกจะเสร็จสิ้นเมื่อมีการเรียกใช้โค้ดและคำจำกัดความของฟังก์ชันจะถูกเรียกใช้ ... เมื่อกำหนดฟังก์ชัน
เปรียบเทียบสิ่งนี้:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
รหัสนี้ได้รับความทุกข์ทรมานจากเหตุการณ์ที่ไม่คาดคิดเดียวกัน กล้วยเป็นแอตทริบิวต์ของคลาสดังนั้นเมื่อคุณเพิ่มสิ่งต่างๆลงไปมันจะเพิ่มลงในอินสแตนซ์ทั้งหมดของคลาสนั้น เหตุผลเหมือนกันเป๊ะ
มันเป็นเพียง "วิธีการทำงาน" และการทำให้มันทำงานแตกต่างกันในกรณีของฟังก์ชันนั้นอาจจะซับซ้อนและในกรณีของคลาสอาจเป็นไปไม่ได้หรืออย่างน้อยก็ทำให้การสร้างอินสแตนซ์วัตถุช้าลงมากเนื่องจากคุณจะต้องเก็บรหัสคลาสไว้รอบ ๆ และดำเนินการเมื่อวัตถุถูกสร้างขึ้น
ใช่มันเป็นเรื่องที่ไม่คาดคิด แต่เมื่อเงินลดลงมันก็เข้ากันได้ดีกับวิธีการทำงานของ Python โดยทั่วไป ในความเป็นจริงมันเป็นเครื่องมือช่วยสอนที่ดีและเมื่อคุณเข้าใจว่าทำไมสิ่งนี้จึงเกิดขึ้นคุณจะยิ่งดีขึ้นมาก
ที่กล่าวว่าควรมีคุณลักษณะเด่นชัดในบทช่วยสอน Python ที่ดี เพราะอย่างที่คุณพูดถึงทุกคนเจอปัญหานี้ไม่ช้าก็เร็ว
ไม่รู้จะวิปัสสนาทำไม
ฉันแปลกใจจริงๆที่ไม่มีใครทำการวิปัสสนาอย่างลึกซึ้งที่ Python ( 2
และ3
นำไปใช้) มาใช้กับ callables
รับฟังก์ชั่นเล็ก ๆ น้อย ๆ ที่func
กำหนดไว้เป็น:
>>> def func(a = []):
... a.append(5)
เมื่อ Python พบสิ่งแรกที่ต้องทำคือคอมไพล์เพื่อสร้างcode
อ็อบเจกต์สำหรับฟังก์ชันนี้ ขณะที่ขั้นตอนการรวบรวมนี้จะทำหลามประเมิน * แล้วเก็บข้อโต้แย้งเริ่มต้น (รายการที่ว่างเปล่า[]
นี่) ในวัตถุฟังก์ชั่นของตัวเอง เป็นคำตอบด้านบนที่กล่าวถึง: รายการa
ในขณะนี้สามารถรับการพิจารณาเป็นสมาชิกfunc
ของฟังก์ชั่น
ลองมาทำวิปัสสนาก่อนและหลังเพื่อตรวจสอบว่ารายการขยายภายในวัตถุฟังก์ชันอย่างไร ฉันใช้Python 3.x
สำหรับสิ่งนี้สำหรับ Python 2 ใช้เหมือนกัน (ใช้__defaults__
หรือfunc_defaults
ใน Python 2 ใช่สองชื่อสำหรับสิ่งเดียวกัน)
ฟังก์ชันก่อนดำเนินการ:
>>> def func(a = []):
... a.append(5)
...
หลังจาก Python รันคำจำกัดความนี้จะใช้พารามิเตอร์เริ่มต้นใด ๆ ที่ระบุ ( a = []
ที่นี่) และอัดไว้ใน__defaults__แอตทริบิวต์สำหรับวัตถุฟังก์ชัน (ส่วนที่เกี่ยวข้อง: Callables):
>>> func.__defaults__
([],)
โอเครายการว่างเป็นรายการเดียว__defaults__
ตามที่คาดไว้
ฟังก์ชั่นหลังจากดำเนินการ:
ตอนนี้ให้เรียกใช้ฟังก์ชันนี้:
>>> func()
ตอนนี้เรามาดูสิ่งเหล่านี้__defaults__
อีกครั้ง:
>>> func.__defaults__
([5],)
ประหลาดใจ? มูลค่าภายในวัตถุเปลี่ยนไป! การเรียกใช้ฟังก์ชันอย่างต่อเนื่องจะต่อท้ายlist
วัตถุฝังนั้น:
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
ดังนั้นคุณมีมันสาเหตุที่ทำให้เกิด'ข้อบกพร่อง'นี้เป็นเพราะอาร์กิวเมนต์เริ่มต้นเป็นส่วนหนึ่งของวัตถุฟังก์ชัน ไม่มีอะไรแปลก ๆ เกิดขึ้นที่นี่มันน่าแปลกใจเล็กน้อย
วิธีแก้ปัญหาทั่วไปในการต่อสู้กับปัญหานี้คือการใช้None
เป็นค่าเริ่มต้นจากนั้นเริ่มต้นในส่วนของฟังก์ชัน:
def func(a = None):
# or: a = [] if a is None else a
if a is None:
a = []
a
เนื่องจากร่างกายฟังก์ชั่นที่จะดำเนินการอีกครั้งในแต่ละครั้งที่คุณเคยได้รับสดรายการใหม่ที่ว่างเปล่าถ้าไม่มีข้อโต้แย้งก็ผ่านไปได้สำหรับ
หากต้องการตรวจสอบเพิ่มเติมว่ารายการใน__defaults__
นั้นเหมือนกับรายการที่ใช้ในฟังก์ชันfunc
คุณสามารถเปลี่ยนฟังก์ชันเพื่อส่งคืนid
รายการที่a
ใช้ภายในเนื้อหาของฟังก์ชันได้ จากนั้นเปรียบเทียบกับรายการใน__defaults__
(ตำแหน่ง[0]
ใน__defaults__
) และคุณจะเห็นว่าสิ่งเหล่านี้อ้างถึงอินสแตนซ์รายการเดียวกันอย่างไร:
>>> def func(a = []):
... a.append(5)
... return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
ด้วยอานุภาพแห่งวิปัสสนา!
*ในการตรวจสอบว่า Python ประเมินอาร์กิวเมนต์เริ่มต้นระหว่างการรวบรวมฟังก์ชันให้ลองดำเนินการดังต่อไปนี้:
def bar(a=input('Did you just see me without calling the function?')):
pass # use raw_input in Py2
ดังที่คุณจะสังเกตเห็นจะinput()
ถูกเรียกก่อนกระบวนการสร้างฟังก์ชันและผูกเข้ากับชื่อbar
จะถูกสร้างขึ้น
ฉันเคยคิดว่าการสร้างวัตถุในรันไทม์จะเป็นแนวทางที่ดีกว่า ตอนนี้ฉันไม่ค่อยมั่นใจนักเนื่องจากคุณสูญเสียคุณสมบัติที่มีประโยชน์บางอย่างไปแม้ว่ามันอาจจะคุ้มค่าไม่ว่าจะเพื่อป้องกันความสับสนของมือใหม่ก็ตาม ข้อเสียของการทำเช่นนี้คือ:
1. ประสิทธิภาพ
def foo(arg=something_expensive_to_compute())):
...
หากใช้การประเมินเวลาโทรระบบจะเรียกฟังก์ชันราคาแพงทุกครั้งที่ใช้ฟังก์ชันของคุณโดยไม่มีอาร์กิวเมนต์ คุณอาจจ่ายราคาแพงในการโทรแต่ละครั้งหรือต้องการแคชค่าภายนอกด้วยตนเองทำให้เนมสเปซของคุณเป็นมลพิษและเพิ่มความฟุ่มเฟื่อย
2. บังคับพารามิเตอร์ที่ถูกผูกไว้
เคล็ดลับที่มีประโยชน์คือการผูกพารามิเตอร์ของแลมบ์ดากับการโยงปัจจุบันของตัวแปรเมื่อสร้างแลมบ์ดา ตัวอย่างเช่น:
funcs = [ lambda i=i: i for i in range(10)]
ส่งคืนรายการฟังก์ชันที่ส่งคืน 0,1,2,3 ... ตามลำดับ หากลักษณะการทำงานที่มีการเปลี่ยนแปลงที่พวกเขาแทนจะผูกi
กับการเรียกร้องเวลา9
ค่าของฉันดังนั้นคุณจะได้รับรายชื่อของฟังก์ชั่นที่ทุกคนกลับมา
วิธีเดียวที่จะนำสิ่งนี้ไปใช้เป็นอย่างอื่นคือการสร้างการปิดเพิ่มเติมด้วย i bound นั่นคือ:
def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]
3. วิปัสสนา
พิจารณารหัส:
def foo(a='test', b=100, c=[]):
print a,b,c
เราสามารถรับข้อมูลเกี่ยวกับอาร์กิวเมนต์และค่าเริ่มต้นโดยใช้inspect
โมดูลซึ่ง
>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))
ข้อมูลนี้มีประโยชน์มากสำหรับสิ่งต่างๆเช่นการสร้างเอกสารการเขียนโปรแกรมเมตาการตกแต่ง ฯลฯ
ตอนนี้สมมติว่าพฤติกรรมของค่าเริ่มต้นสามารถเปลี่ยนแปลงได้เพื่อให้เทียบเท่ากับ:
_undefined = object() # sentinel value
def foo(a=_undefined, b=_undefined, c=_undefined)
if a is _undefined: a='test'
if b is _undefined: b=100
if c is _undefined: c=[]
อย่างไรก็ตามเราสูญเสียความสามารถในการไตร่ตรองและดูว่าอาร์กิวเมนต์เริ่มต้นคืออะไร เนื่องจากยังไม่ได้สร้างวัตถุเราจึงไม่สามารถจับวัตถุเหล่านั้นได้โดยไม่เรียกใช้ฟังก์ชันจริงๆ สิ่งที่ดีที่สุดที่เราทำได้คือเก็บซอร์สโค้ดและส่งคืนเป็นสตริง
5 คะแนนในการป้องกัน Python
ความเรียบง่าย : พฤติกรรมนั้นเรียบง่ายในแง่ต่อไปนี้: คนส่วนใหญ่ตกหลุมพรางนี้เพียงครั้งเดียวไม่ใช่หลายครั้ง
ความสอดคล้อง : Python ส่งผ่านวัตถุเสมอไม่ใช่ชื่อ เห็นได้ชัดว่าพารามิเตอร์เริ่มต้นคือส่วนหนึ่งของส่วนหัวของฟังก์ชัน (ไม่ใช่ส่วนของฟังก์ชัน) ดังนั้นจึงควรได้รับการประเมินที่เวลาโหลดโมดูล (และเฉพาะที่เวลาโหลดโมดูลเว้นแต่จะซ้อนกัน) ไม่ใช่ในเวลาเรียกใช้ฟังก์ชัน
ประโยชน์ : ดังที่ Frederik Lundh ชี้ให้เห็นในคำอธิบายของเขาเกี่ยวกับ"Default Parameter Values in Python"พฤติกรรมปัจจุบันอาจมีประโยชน์มากสำหรับการเขียนโปรแกรมขั้นสูง (ใช้เท่าที่จำเป็น)
เอกสารเพียงพอ : ในเอกสารหลามพื้นฐานที่สุด, กวดวิชาที่ปัญหาจะมีการประกาศเสียงดังเป็น"คำเตือนที่สำคัญ"ในครั้งแรกส่วนย่อยของมาตรา"เพิ่มเติมเกี่ยวกับการกำหนดฟังก์ชั่น" คำเตือนยังใช้ตัวหนาซึ่งแทบไม่ได้ใช้นอกหัวเรื่อง RTFM: อ่านคู่มืออย่างละเอียด
Meta-learning : การตกอยู่ในกับดักเป็นช่วงเวลาที่มีประโยชน์มาก (อย่างน้อยถ้าคุณเป็นผู้เรียนที่ไตร่ตรอง) เพราะหลังจากนั้นคุณจะเข้าใจประเด็น "ความสม่ำเสมอ" ข้างต้นได้ดีขึ้นและนั่นจะสอนคุณได้มากเกี่ยวกับ Python
พฤติกรรมนี้อธิบายได้ง่ายโดย:
- การประกาศฟังก์ชัน (คลาส ฯลฯ ) จะดำเนินการเพียงครั้งเดียวโดยสร้างอ็อบเจ็กต์ค่าเริ่มต้นทั้งหมด
- ทุกอย่างผ่านการอ้างอิง
ดังนั้น:
def x(a=0, b=[], c=[], d=0):
a = a + 1
b = b + [1]
c.append(1)
print a, b, c
a
ไม่เปลี่ยนแปลง - ทุกการเรียกมอบหมายจะสร้างวัตถุ int ใหม่ - วัตถุใหม่จะถูกพิมพ์b
ไม่เปลี่ยนแปลง - อาร์เรย์ใหม่สร้างจากค่าเริ่มต้นและพิมพ์c
การเปลี่ยนแปลง - ดำเนินการกับวัตถุเดียวกัน - และพิมพ์ออกมา
สิ่งที่คุณถามคือสาเหตุนี้:
def func(a=[], b = 2):
pass
ไม่เทียบเท่ากับสิ่งนี้ภายใน:
def func(a=None, b = None):
a_default = lambda: []
b_default = lambda: 2
def actual_func(a=None, b=None):
if a is None: a = a_default()
if b is None: b = b_default()
return actual_func
func = func()
ยกเว้นในกรณีของการเรียก func อย่างชัดเจน (ไม่มี, ไม่มี) ซึ่งเราจะไม่สนใจ
กล่าวอีกนัยหนึ่งคือแทนที่จะประเมินพารามิเตอร์เริ่มต้นทำไมไม่เก็บแต่ละพารามิเตอร์ไว้และประเมินค่าเมื่อฟังก์ชันถูกเรียกใช้
คำตอบเดียวน่าจะอยู่ที่นั่น - มันจะเปลี่ยนทุกฟังก์ชันที่มีพารามิเตอร์เริ่มต้นเป็นการปิดอย่างมีประสิทธิภาพ แม้ว่าข้อมูลทั้งหมดจะซ่อนอยู่ในล่ามและไม่ใช่การปิดแบบเต็มรูปแบบข้อมูลก็จะต้องถูกเก็บไว้ที่ใดที่หนึ่ง มันจะช้าลงและใช้หน่วยความจำมากขึ้น
1) ปัญหาที่เรียกว่า "Mutable Default Argument" โดยทั่วไปเป็นตัวอย่างพิเศษที่แสดงให้เห็นว่า:
"ฟังก์ชั่นทั้งหมดที่มีปัญหานี้ประสบปัญหาผลข้างเคียงที่คล้ายคลึงกันในพารามิเตอร์จริง "
ซึ่งขัดต่อกฎของการเขียนโปรแกรมเชิงฟังก์ชัน มักจะไม่สามารถคาดเดาได้และควรได้รับการแก้ไขทั้งสองอย่างด้วยกัน
ตัวอย่าง:
def foo(a=[]): # the same problematic function
a.append(5)
return a
>>> somevar = [1, 2] # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5] # usually expected [1, 2]
วิธีแก้ไข : สำเนา
วิธีแก้ปัญหาที่ปลอดภัยอย่างยิ่งคือไปที่copy
หรือdeepcopy
วัตถุป้อนข้อมูลก่อนแล้วจึงจะทำอะไรก็ได้กับสำเนา
def foo(a=[]):
a = a[:] # a copy
a.append(5)
return a # or everything safe by one line: "return a + [5]"
หลายประเภทในตัวไม่แน่นอนมีวิธีการคัดลอกเหมือนsome_dict.copy()
หรือsome_set.copy()
หรือสามารถคัดลอกง่ายเหมือนหรือsomelist[:]
list(some_list)
ทุกออบเจ็กต์สามารถคัดลอกได้โดยcopy.copy(any_object)
ละเอียดมากขึ้นโดยcopy.deepcopy()
(อันหลังมีประโยชน์ถ้าอ็อบเจกต์ที่เปลี่ยนแปลงได้นั้นประกอบขึ้นจากอ็อบเจ็กต์ที่เปลี่ยนแปลงได้) วัตถุบางอย่างมีพื้นฐานมาจากผลข้างเคียงเช่นวัตถุ "ไฟล์" และไม่สามารถทำซ้ำโดยการคัดลอกได้อย่างมีความหมาย การคัดลอก
ตัวอย่างปัญหาสำหรับคำถาม SO ที่คล้ายกัน
class Test(object): # the original problematic class
def __init__(self, var1=[]):
self._var1 = var1
somevar = [1, 2] # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar # [1, 2, [1]] but usually expected [1, 2]
print t2._var1 # [1, 2, [1]] but usually expected [1, 2]
ไม่ควรบันทึกไว้ในแอตทริบิวต์สาธารณะใด ๆของอินสแตนซ์ที่ส่งคืนโดยฟังก์ชันนี้ (สมมติว่าไม่ควรแก้ไขแอตทริบิวต์ส่วนตัวของอินสแตนซ์จากภายนอกคลาสนี้หรือคลาสย่อยโดยอนุสัญญากล่าว_var1
คือแอตทริบิวต์ส่วนตัว)
สรุป:
ไม่ควรแก้ไขออบเจ็กต์พารามิเตอร์อินพุต (กลายพันธุ์) และไม่ควรผูกเข้ากับอ็อบเจ็กต์ที่ส่งคืนโดยฟังก์ชัน (หากเราต้องการเขียนโปรแกรมโดยไม่มีผลข้างเคียงซึ่งขอแนะนำอย่างยิ่งโปรดดูWiki เกี่ยวกับ "side effect" (สองย่อหน้าแรกเกี่ยวข้องกับบริบทนี้)
2)
เฉพาะในกรณีที่จำเป็นต้องใช้ผลข้างเคียงกับพารามิเตอร์จริง แต่ไม่ต้องการในพารามิเตอร์เริ่มต้นทางออกที่มีประโยชน์คือdef ...(var1=None):
if var1 is None:
var1 = []
More ..
3) ในบางกรณีเป็นพฤติกรรมที่ไม่แน่นอนของค่าเริ่มต้นที่มีประโยชน์
จริงๆแล้วสิ่งนี้ไม่เกี่ยวข้องกับค่าเริ่มต้นนอกจากนั้นมักเกิดขึ้นเป็นลักษณะการทำงานที่ไม่คาดคิดเมื่อคุณเขียนฟังก์ชันด้วยค่าเริ่มต้นที่ไม่แน่นอน
>>> def foo(a):
a.append(5)
print a
>>> a = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]
ไม่มีค่าเริ่มต้นในรหัสนี้ แต่คุณพบปัญหาเดียวกัน
ปัญหาคือfoo
กำลังแก้ไขตัวแปรที่เปลี่ยนแปลงได้ที่ส่งผ่านจากผู้โทรเมื่อผู้โทรไม่คาดหวังสิ่งนี้ รหัสเช่นนี้จะดีถ้าฟังก์ชั่นที่เรียกว่าสิ่งที่ต้องการappend_5
; จากนั้นผู้โทรจะเรียกใช้ฟังก์ชันเพื่อแก้ไขค่าที่ส่งผ่านและคาดว่าจะเกิดพฤติกรรม แต่ฟังก์ชั่นดังกล่าวไม่น่าจะเป็นไปได้มากที่จะใช้อาร์กิวเมนต์เริ่มต้นและอาจไม่ส่งคืนรายการ (เนื่องจากผู้โทรมีการอ้างอิงถึงรายการนั้นอยู่แล้วคนที่เพิ่งส่งผ่าน)
ต้นฉบับของคุณที่foo
มีอาร์กิวเมนต์เริ่มต้นไม่ควรแก้ไขa
ไม่ว่าจะส่งผ่านอย่างชัดเจนหรือได้รับค่าเริ่มต้น รหัสของคุณควรปล่อยให้อาร์กิวเมนต์ที่ไม่แน่นอนเพียงอย่างเดียวเว้นแต่จะมีความชัดเจนจากบริบท / ชื่อ / เอกสารประกอบว่าอาร์กิวเมนต์ควรได้รับการแก้ไข การใช้ค่าที่ไม่แน่นอนที่ส่งผ่านเป็นอาร์กิวเมนต์เนื่องจากเทมโมไลบรารีในเครื่องเป็นความคิดที่ไม่ดีอย่างยิ่งไม่ว่าเราจะอยู่ใน Python หรือไม่และมีอาร์กิวเมนต์เริ่มต้นที่เกี่ยวข้องหรือไม่
หากคุณต้องการจัดการชั่วคราวในเครื่องอย่างทำลายล้างในระหว่างการคำนวณบางอย่างและคุณจำเป็นต้องเริ่มการจัดการจากค่าอาร์กิวเมนต์คุณต้องทำสำเนา
หัวข้อยุ่งอยู่แล้ว แต่จากสิ่งที่ฉันอ่านที่นี่สิ่งต่อไปนี้ช่วยให้ฉันรู้ว่ามันทำงานอย่างไรภายใน:
def bar(a=[]):
print id(a)
a = a + [1]
print id(a)
return a
>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object
[1]
>>> id(bar.func_defaults[0])
4484370232
เป็นการเพิ่มประสิทธิภาพ จากผลของฟังก์ชันนี้คุณคิดว่าการเรียกใช้ฟังก์ชันใดในสองฟังก์ชันนี้เร็วกว่ากัน
def print_tuple(some_tuple=(1,2,3)):
print some_tuple
print_tuple() #1
print_tuple((1,2,3)) #2
ฉันจะให้คำใบ้ นี่คือการถอดชิ้นส่วน (ดูhttp://docs.python.org/library/dis.html):
#
1
0 LOAD_GLOBAL 0 (print_tuple)
3 CALL_FUNCTION 0
6 POP_TOP
7 LOAD_CONST 0 (None)
10 RETURN_VALUE
#
2
0 LOAD_GLOBAL 0 (print_tuple)
3 LOAD_CONST 4 ((1, 2, 3))
6 CALL_FUNCTION 1
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
ฉันสงสัยว่าพฤติกรรมที่มีประสบการณ์มีการใช้งานจริง (ใครใช้ตัวแปรคงที่ใน C โดยไม่มีข้อบกพร่องในการผสมพันธุ์?)
ในขณะที่คุณสามารถดูมีเป็นประโยชน์ประสิทธิภาพการทำงานเมื่อเริ่มต้นใช้อาร์กิวเมนต์ไม่เปลี่ยนรูป สิ่งนี้สามารถสร้างความแตกต่างได้หากเป็นฟังก์ชันที่เรียกบ่อยหรืออาร์กิวเมนต์เริ่มต้นใช้เวลาสร้างนาน นอกจากนี้โปรดทราบว่า Python ไม่ใช่ C ใน C คุณมีค่าคงที่ค่อนข้างฟรี ใน Python คุณไม่มีประโยชน์นี้
Python: อาร์กิวเมนต์เริ่มต้นที่เปลี่ยนแปลงได้
อาร์กิวเมนต์ดีฟอลต์จะได้รับการประเมินในเวลาที่ฟังก์ชันถูกคอมไพล์เป็นอ็อบเจ็กต์ฟังก์ชัน เมื่อใช้งานโดยฟังก์ชันหลาย ๆ ครั้งโดยฟังก์ชันนั้นจะเป็นและยังคงเป็นวัตถุเดิม
เมื่อมีการเปลี่ยนแปลงเมื่อกลายพันธุ์ (ตัวอย่างเช่นโดยการเพิ่มองค์ประกอบเข้าไป) พวกมันจะยังคงกลายพันธุ์เมื่อมีการโทรติดต่อกัน
พวกมันกลายพันธุ์อยู่เสมอเพราะเป็นวัตถุเดียวกันในแต่ละครั้ง
รหัสเทียบเท่า:
เนื่องจากรายการถูกผูกไว้กับฟังก์ชันเมื่อวัตถุฟังก์ชันถูกคอมไพล์และสร้างอินสแตนซ์สิ่งนี้:
def foo(mutable_default_argument=[]): # make a list the default argument
"""function that uses a list"""
เกือบจะเทียบเท่ากับสิ่งนี้:
_a_list = [] # create a list in the globals
def foo(mutable_default_argument=_a_list): # make it the default argument
"""function that uses a list"""
del _a_list # remove globals name binding
สาธิต
นี่คือการสาธิต - คุณสามารถตรวจสอบได้ว่าเป็นวัตถุเดียวกันทุกครั้งที่อ้างอิงโดย
- เมื่อเห็นว่ารายการถูกสร้างขึ้นก่อนที่ฟังก์ชันจะเสร็จสิ้นการคอมไพล์ไปยังวัตถุฟังก์ชัน
- สังเกตว่า id จะเหมือนกันทุกครั้งที่มีการอ้างอิงรายการ
- สังเกตว่ารายการยังคงเปลี่ยนไปเมื่อฟังก์ชันที่ใช้เรียกเป็นครั้งที่สอง
- สังเกตลำดับที่พิมพ์เอาต์พุตจากแหล่งที่มา (ซึ่งฉันสะดวกสำหรับคุณ):
example.py
print('1. Global scope being evaluated')
def create_list():
'''noisily create a list for usage as a kwarg'''
l = []
print('3. list being created and returned, id: ' + str(id(l)))
return l
print('2. example_function about to be compiled to an object')
def example_function(default_kwarg1=create_list()):
print('appending "a" in default default_kwarg1')
default_kwarg1.append("a")
print('list with id: ' + str(id(default_kwarg1)) +
' - is now: ' + repr(default_kwarg1))
print('4. example_function compiled: ' + repr(example_function))
if __name__ == '__main__':
print('5. calling example_function twice!:')
example_function()
example_function()
และเรียกใช้ด้วยpython example.py
:
1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']
สิ่งนี้ละเมิดหลักการ "ความประหลาดใจน้อยที่สุด" หรือไม่?
ลำดับการดำเนินการนี้มักสร้างความสับสนให้กับผู้ใช้ Python ใหม่ หากคุณเข้าใจรูปแบบการดำเนินการของ Python แล้วก็เป็นที่คาดหวัง
คำแนะนำปกติสำหรับผู้ใช้ Python ใหม่:
แต่นี่คือสาเหตุที่คำสั่งตามปกติสำหรับผู้ใช้ใหม่คือการสร้างอาร์กิวเมนต์เริ่มต้นเช่นนี้แทน:
def example_function_2(default_kwarg=None):
if default_kwarg is None:
default_kwarg = []
สิ่งนี้ใช้ None singleton เป็นวัตถุยามเพื่อบอกฟังก์ชันว่าเราได้รับอาร์กิวเมนต์อื่นที่ไม่ใช่ค่าเริ่มต้นหรือไม่ หากเราไม่ได้รับอาร์กิวเมนต์แสดงว่าเราต้องการใช้รายการว่างใหม่[]
เป็นค่าเริ่มต้น
ดังที่ส่วนการสอนเกี่ยวกับโฟลว์การควบคุมกล่าวว่า:
หากคุณไม่ต้องการใช้ค่าเริ่มต้นร่วมกันระหว่างการโทรครั้งต่อ ๆ ไปคุณสามารถเขียนฟังก์ชันเช่นนี้แทนได้:
def f(a, L=None): if L is None: L = [] L.append(a) return L
คำตอบที่สั้นที่สุดน่าจะเป็น "คำจำกัดความคือการดำเนินการ" ดังนั้นการโต้แย้งทั้งหมดจึงไม่มีเหตุผลที่เข้มงวด เป็นตัวอย่างที่ได้รับการออกแบบเพิ่มเติมคุณสามารถอ้างอิงสิ่งนี้:
def a(): return []
def b(x=a()):
print x
หวังว่าจะเพียงพอที่จะแสดงให้เห็นว่าการไม่เรียกใช้นิพจน์อาร์กิวเมนต์เริ่มต้นในเวลาดำเนินการของdef
คำสั่งนั้นไม่ใช่เรื่องง่ายหรือไม่สมเหตุสมผลหรือทั้งสองอย่าง
ฉันยอมรับว่ามันเป็น gotcha เมื่อคุณพยายามใช้ตัวสร้างเริ่มต้น
พฤติกรรมนี้ไม่น่าแปลกใจหากคุณคำนึงถึงสิ่งต่อไปนี้:
- ลักษณะการทำงานของแอตทริบิวต์คลาสแบบอ่านอย่างเดียวเมื่อพยายามมอบหมายและสิ่งนั้น
- ฟังก์ชั่นคือวัตถุ (อธิบายได้ดีในคำตอบที่ยอมรับ)
บทบาทของ(2)ได้รับการกล่าวถึงอย่างกว้างขวางในชุดข้อความนี้ (1)น่าจะเป็นปัจจัยที่ทำให้เกิดความประหลาดใจเนื่องจากพฤติกรรมนี้ไม่ "เข้าใจง่าย" เมื่อมาจากภาษาอื่น
(1)อธิบายไว้ในหลามสอนในชั้นเรียน ในความพยายามที่จะกำหนดค่าให้กับแอตทริบิวต์คลาสแบบอ่านอย่างเดียว:
... ตัวแปรทั้งหมดที่พบนอกขอบเขตด้านในสุดเป็นแบบอ่านอย่างเดียว ( การพยายามเขียนถึงตัวแปรดังกล่าวจะเป็นการสร้างตัวแปรท้องถิ่นใหม่ในขอบเขตด้านในสุดโดยปล่อยให้ตัวแปรภายนอกที่มีชื่อเหมือนกันไม่เปลี่ยนแปลง )
ย้อนกลับไปดูตัวอย่างเดิมและพิจารณาประเด็นข้างต้น:
def foo(a=[]):
a.append(5)
return a
นี่foo
คือวัตถุและa
เป็นคุณลักษณะของfoo
(มีให้ที่foo.func_defs[0]
) เนื่องจากa
เป็นรายการที่ไม่แน่นอนและจึงแอตทริบิวต์อ่านเขียนของa
foo
เริ่มต้นเป็นรายการว่างตามที่ระบุโดยลายเซ็นเมื่อฟังก์ชันถูกสร้างอินสแตนซ์และพร้อมใช้งานสำหรับการอ่านและเขียนตราบเท่าที่มีวัตถุฟังก์ชันอยู่
โทรโดยไม่ต้องเอาชนะเริ่มต้นใช้ค่าเริ่มต้นของที่มาจากfoo
foo.func_defs
ในกรณีfoo.func_defs[0]
นี้ใช้สำหรับa
ภายในขอบเขตโค้ดของอ็อบเจ็กต์ฟังก์ชัน การเปลี่ยนแปลงa
การเปลี่ยนแปลงfoo.func_defs[0]
ซึ่งเป็นส่วนหนึ่งของวัตถุและยังคงอยู่ระหว่างการดำเนินการของรหัสในfoo
foo
ตอนนี้ให้เปรียบเทียบสิ่งนี้กับตัวอย่างจากเอกสารเกี่ยวกับการเลียนแบบพฤติกรรมอาร์กิวเมนต์เริ่มต้นของภาษาอื่นเช่นค่าเริ่มต้นลายเซ็นของฟังก์ชันจะถูกใช้ทุกครั้งที่เรียกใช้ฟังก์ชัน:
def foo(a, L=None):
if L is None:
L = []
L.append(a)
return L
เมื่อพิจารณาถึงข้อ(1)และ(2)เราจะเห็นได้ว่าเหตุใดสิ่งนี้จึงบรรลุพฤติกรรมที่ต้องการ:
- เมื่อ
foo
อ็อบเจ็กต์ฟังก์ชันถูกสร้างอินสแตนซ์foo.func_defs[0]
ถูกตั้งค่าNone
เป็นอ็อบเจ็กต์ที่ไม่เปลี่ยนรูป - เมื่อฟังก์ชั่นที่จะดำเนินการกับค่าเริ่มต้น (กับพารามิเตอร์ไม่มีระบุไว้สำหรับ
L
ในการเรียกฟังก์ชั่น)foo.func_defs[0]
(None
)L
มีให้บริการในขอบเขตท้องถิ่น - เมื่อ
L = []
มอบหมายงานไม่สำเร็จfoo.func_defs[0]
เนื่องจากแอตทริบิวต์นั้นเป็นแบบอ่านอย่างเดียว - ต่อ(1) , ตัวแปรท้องถิ่นใหม่ชื่อยัง
L
ถูกสร้างขึ้นในขอบเขตท้องถิ่นและใช้สำหรับส่วนที่เหลือของการเรียกฟังก์ชั่น จึงยังคงไม่เปลี่ยนแปลงสำหรับสวดในอนาคตของfoo.func_defs[0]
foo
วิธีแก้ปัญหาง่ายๆโดยใช้ไม่มี
>>> def bar(b, data=None):
... data = data or []
... data.append(b)
... return data
...
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
ฉันจะสาธิตโครงสร้างทางเลือกเพื่อส่งผ่านค่ารายการเริ่มต้นไปยังฟังก์ชัน (ทำงานได้ดีพอ ๆ กันกับพจนานุกรม)
ตามที่คนอื่น ๆ ได้แสดงความคิดเห็นไว้อย่างกว้างขวางพารามิเตอร์ list จะถูกผูกไว้กับฟังก์ชันเมื่อมีการกำหนดซึ่งตรงข้ามกับเมื่อเรียกใช้งาน เนื่องจากรายการและพจนานุกรมไม่แน่นอนการเปลี่ยนแปลงใด ๆ กับพารามิเตอร์นี้จะส่งผลต่อการเรียกใช้ฟังก์ชันนี้อื่น ๆ ด้วยเหตุนี้การเรียกใช้ฟังก์ชันในภายหลังจะได้รับรายการที่ใช้ร่วมกันซึ่งอาจถูกเปลี่ยนแปลงโดยการเรียกใช้ฟังก์ชันอื่น ยิ่งไปกว่านั้นพารามิเตอร์สองตัวกำลังใช้พารามิเตอร์ที่ใช้ร่วมกันของฟังก์ชันนี้ในเวลาเดียวกันโดยไม่สนใจการเปลี่ยนแปลงที่ทำโดยอีกพารามิเตอร์หนึ่ง
ผิดวิธี (อาจจะ ... ) :
def foo(list_arg=[5]):
return list_arg
a = foo()
a.append(6)
>>> a
[5, 6]
b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]
# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()
7
คุณสามารถตรวจสอบได้ว่าเป็นวัตถุเดียวกันโดยใช้id
:
>>> id(a)
5347866528
>>> id(b)
5347866528
Per Brett Slatkin's "Effective Python: 59 วิธีเฉพาะในการเขียน Python ที่ดีขึ้น", รายการ 20: ใช้None
และ Docstrings เพื่อระบุอาร์กิวเมนต์ดีฟอลต์แบบไดนามิก (น. 48)
หลักการในการบรรลุผลลัพธ์ที่ต้องการใน Python คือการระบุค่าเริ่มต้นของ
None
และเพื่อบันทึกพฤติกรรมจริงใน docstring
การใช้งานนี้ช่วยให้มั่นใจได้ว่าการเรียกใช้ฟังก์ชันแต่ละครั้งจะได้รับรายการเริ่มต้นหรือมิฉะนั้นรายการจะส่งผ่านไปยังฟังก์ชัน
วิธีที่ต้องการ :
def foo(list_arg=None):
"""
:param list_arg: A list of input values.
If none provided, used a list with a default value of 5.
"""
if not list_arg:
list_arg = [5]
return list_arg
a = foo()
a.append(6)
>>> a
[5, 6]
b = foo()
b.append(7)
>>> b
[5, 7]
c = foo([10])
c.append(11)
>>> c
[10, 11]
อาจมีกรณีการใช้งานที่ถูกต้องสำหรับ 'วิธีการที่ไม่ถูกต้อง' โดยโปรแกรมเมอร์ตั้งใจให้พารามิเตอร์รายการเริ่มต้นใช้ร่วมกัน แต่มีแนวโน้มว่าจะเป็นข้อยกเว้นมากกว่ากฎ
วิธีแก้ปัญหาที่นี่คือ:
- ใช้
None
เป็นค่าเริ่มต้นของคุณ (หรือ nonceobject
) และเปิดใช้งานเพื่อสร้างค่าของคุณในรันไทม์ หรือ - ใช้ a
lambda
เป็นพารามิเตอร์เริ่มต้นของคุณและเรียกมันภายใน try block เพื่อรับค่าเริ่มต้น (นี่คือประเภทของสิ่งที่แลมบ์ดานามธรรมมีไว้สำหรับ)
ตัวเลือกที่สองเป็นสิ่งที่ดีเนื่องจากผู้ใช้ฟังก์ชั่นสามารถส่งผ่านใน callable ซึ่งอาจมีอยู่แล้ว (เช่น a type
)
คุณสามารถหลีกเลี่ยงสิ่งนี้ได้โดยการแทนที่วัตถุ (ดังนั้นการผูกด้วยขอบเขต):
def foo(a=[]):
a = list(a)
a.append(5)
return a
น่าเกลียด แต่ได้ผล
เมื่อเราทำสิ่งนี้:
def foo(a=[]):
...
... เรากำหนดอาร์กิวเมนต์a
ให้กับรายการที่ไม่มีชื่อหากผู้โทรไม่ส่งค่าของ.
เพื่อให้การสนทนานี้ง่ายขึ้นเรามาตั้งชื่อรายการที่ไม่มีชื่อชั่วคราว แล้วไงpavlo
?
def foo(a=pavlo):
...
ณ เวลาใด ๆ ถ้าโทรไม่ได้บอกเราว่าสิ่งที่เป็นเรานำมาใช้ใหม่a
pavlo
ถ้าpavlo
เป็นไม่แน่นอน (แก้ไขได้) และfoo
จบลงด้วยการปรับเปลี่ยนมันมีผลเราแจ้งให้ทราบในครั้งต่อไปจะเรียกว่าไม่ต้องระบุfoo
a
นี่คือสิ่งที่คุณเห็น (โปรดจำไว้ว่าpavlo
เริ่มต้นเป็น []):
>>> foo()
[5]
ตอนนี้pavlo
คือ [5]
การโทรfoo()
อีกครั้งแก้ไขpavlo
อีกครั้ง:
>>> foo()
[5, 5]
ระบุa
เมื่อเรียกfoo()
ความมั่นใจว่าpavlo
จะไม่ได้สัมผัส
>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
ดังนั้นยังคงเป็นpavlo
[5, 5]
>>> foo()
[5, 5, 5]
บางครั้งฉันใช้ประโยชน์จากพฤติกรรมนี้แทนรูปแบบต่อไปนี้:
singleton = None
def use_singleton():
global singleton
if singleton is None:
singleton = _make_singleton()
return singleton.use_me()
หากsingleton
ใช้โดยuse_singleton
ฉันชอบรูปแบบต่อไปนี้แทน:
# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
return singleton.use_me()
ฉันใช้สิ่งนี้เพื่อสร้างอินสแตนซ์คลาสไคลเอนต์ที่เข้าถึงทรัพยากรภายนอกและสำหรับการสร้างคำสั่งหรือรายการสำหรับบันทึกช่วยจำ
เนื่องจากฉันไม่คิดว่ารูปแบบนี้เป็นที่รู้จักกันดีฉันจึงแสดงความคิดเห็นสั้น ๆ เพื่อป้องกันความเข้าใจผิดในอนาคต
อาจเป็นความจริงที่:
- มีคนใช้ทุกภาษา / คุณลักษณะไลบรารีและ
- การเปลี่ยนพฤติกรรมที่นี่อาจเป็นคำแนะนำที่ไม่ดี แต่
มันสอดคล้องกันอย่างสิ้นเชิงที่จะยึดคุณสมบัติทั้งสองข้างต้นและยังคงให้ประเด็นอื่น:
- มันเป็นคุณสมบัติที่สับสนและโชคร้ายใน Python
คำตอบอื่น ๆ หรืออย่างน้อยบางคำก็ให้คะแนน 1 และ 2 แต่ไม่ใช่ 3 หรือทำคะแนน 3 และดาวน์เพลย์คะแนน 1 และ 2 แต่ทั้งสามเป็นความจริง
อาจเป็นเรื่องจริงที่การเปลี่ยนม้าในช่วงกลางน้ำที่นี่จะขอให้มีการแตกหักอย่างมีนัยสำคัญและอาจมีปัญหามากขึ้นจากการเปลี่ยน Python เพื่อจัดการตัวอย่างการเปิดของ Stefano โดยสังหรณ์ใจ และอาจเป็นเรื่องจริงที่ใครบางคนที่รู้จัก Python internalals เป็นอย่างดีสามารถอธิบายถึงทุ่นระเบิดของผลที่ตามมาได้ อย่างไรก็ตาม
พฤติกรรมที่มีอยู่ไม่ใช่ Pythonic และ Python ประสบความสำเร็จเนื่องจากมีน้อยมากเกี่ยวกับภาษาที่ละเมิดหลักการของความประหลาดใจน้อยที่สุดที่ใดก็ตามที่อยู่ใกล้สิ่งนี้ไม่ดี เป็นปัญหาที่แท้จริงไม่ว่าจะเป็นการฉลาดหรือไม่ที่จะถอนรากถอนโคน มันเป็นข้อบกพร่องของการออกแบบ หากคุณเข้าใจภาษาได้ดีขึ้นมากโดยพยายามติดตามพฤติกรรมฉันสามารถพูดได้ว่า C ++ ทำสิ่งนี้ทั้งหมดและอื่น ๆ คุณได้เรียนรู้มากมายจากการนำทางตัวอย่างเช่นข้อผิดพลาดของตัวชี้เล็กน้อย แต่นี่ไม่ใช่ Pythonic: คนที่สนใจ Python มากพอที่จะอดทนต่อการเผชิญกับพฤติกรรมนี้คือคนที่สนใจภาษาเพราะ Python มีความประหลาดใจน้อยกว่าภาษาอื่นมาก Dabblers และคนที่อยากรู้อยากเห็นกลายเป็น Pythonistas เมื่อพวกเขาประหลาดใจว่าต้องใช้เวลาเพียงเล็กน้อยในการทำงานบางอย่างไม่ใช่เพราะการออกแบบ - ฉันหมายถึงปริศนาตรรกะที่ซ่อนอยู่ - ซึ่งตัดกับสัญชาตญาณของโปรแกรมเมอร์ที่ดึงดูดความสนใจไปที่ Python เพราะมันใช้งานได้จริง
นี้ไม่ได้เป็นข้อบกพร่องในการออกแบบ ใครก็ตามที่เดินทางไปนี้กำลังทำอะไรผิดพลาด
มี 3 กรณีที่ฉันพบว่าคุณอาจพบปัญหานี้:
- คุณตั้งใจจะแก้ไขอาร์กิวเมนต์เป็นผลข้างเคียงของฟังก์ชัน ในกรณีนี้ไม่มีเหตุผลที่จะมีอาร์กิวเมนต์เริ่มต้น ข้อยกเว้นเพียงอย่างเดียวคือเมื่อคุณใช้รายการอาร์กิวเมนต์ในทางที่ผิดเพื่อให้มีคุณลักษณะของฟังก์ชันเช่น
cache={}
และคุณจะไม่คาดว่าจะเรียกใช้ฟังก์ชันด้วยอาร์กิวเมนต์จริงเลย - คุณตั้งใจจะปล่อยให้อาร์กิวเมนต์ไม่มีการแก้ไข แต่คุณได้แก้ไขโดยบังเอิญ นั่นคือจุดบกพร่องให้แก้ไข
- คุณตั้งใจจะแก้ไขอาร์กิวเมนต์เพื่อใช้ภายในฟังก์ชัน แต่ไม่ได้คาดหวังว่าการปรับเปลี่ยนจะสามารถดูได้ภายนอกฟังก์ชัน ในกรณีนี้คุณต้องทำสำเนาอาร์กิวเมนต์ไม่ว่าจะเป็นค่าเริ่มต้นหรือไม่ก็ตาม! Python ไม่ใช่ภาษาเรียกตามค่าดังนั้นจึงไม่ได้ทำสำเนาให้คุณคุณจำเป็นต้องมีความชัดเจน
ตัวอย่างในคำถามอาจอยู่ในประเภท 1 หรือ 3 เป็นเรื่องแปลกที่ทั้งสองแก้ไขรายการที่ผ่านและส่งคืน คุณควรเลือกอย่างใดอย่างหนึ่ง
"ข้อบกพร่อง" นี้ทำให้ฉันมีชั่วโมงทำงานล่วงเวลามาก! แต่ฉันเริ่มเห็นการใช้งานที่เป็นไปได้ (แต่ฉันก็อยากให้มันเป็นเวลาดำเนินการอยู่ดี)
ฉันจะให้สิ่งที่ฉันเห็นเป็นตัวอย่างที่มีประโยชน์
def example(errors=[]):
# statements
# Something went wrong
mistake = True
if mistake:
tryToFixIt(errors)
# Didn't work.. let's try again
tryToFixItAnotherway(errors)
# This time it worked
return errors
def tryToFixIt(err):
err.append('Attempt to fix it')
def tryToFixItAnotherway(err):
err.append('Attempt to fix it by another way')
def main():
for item in range(2):
errors = example()
print '\n'.join(errors)
main()
พิมพ์สิ่งต่อไปนี้
Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
เพียงแค่เปลี่ยนฟังก์ชั่นเป็น:
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
ฉันคิดว่าคำตอบสำหรับคำถามนี้อยู่ที่วิธีที่ python ส่งผ่านข้อมูลไปยังพารามิเตอร์ (ส่งโดยค่าหรือโดยการอ้างอิง) ไม่ใช่ความไม่แน่นอนหรือวิธีที่ python จัดการกับคำสั่ง "def"
แนะนำสั้น ๆ ประการแรกมีประเภทข้อมูลสองประเภทใน python ประเภทหนึ่งคือประเภทข้อมูลพื้นฐานอย่างง่ายเช่นตัวเลขและประเภทข้อมูลอีกประเภทคือวัตถุ ประการที่สองเมื่อส่งข้อมูลไปยังพารามิเตอร์ python จะส่งชนิดข้อมูลพื้นฐานตามค่ากล่าวคือสร้างสำเนาของค่าในเครื่องไปยังตัวแปรท้องถิ่น แต่ส่งผ่านวัตถุโดยการอ้างอิงเช่นตัวชี้ไปยังวัตถุ
ยอมรับสองประเด็นข้างต้นเรามาอธิบายว่าเกิดอะไรขึ้นกับรหัส python เป็นเพียงเพราะการส่งผ่านโดยการอ้างอิงสำหรับวัตถุ แต่ไม่มีส่วนเกี่ยวข้องกับการเปลี่ยนแปลง / ไม่เปลี่ยนรูปหรือเนื้อหาที่ว่าคำสั่ง "def" จะดำเนินการเพียงครั้งเดียวเมื่อมีการกำหนด
[] เป็นวัตถุดังนั้น python จึงส่งการอ้างอิงของ [] ถึงa
กล่าวa
คือเป็นเพียงตัวชี้ไปที่ [] ซึ่งอยู่ในหน่วยความจำเป็นวัตถุ มีเพียงสำเนาเดียวของ [] ที่มีการอ้างอิงจำนวนมาก สำหรับ foo () แรกรายการ [] จะเปลี่ยนเป็น1โดยวิธีผนวก แต่ทราบว่ามีเพียงหนึ่งสำเนาของวัตถุรายการและวัตถุนี้ตอนนี้กลายเป็น1 เมื่อเรียกใช้ foo () ที่สองสิ่งที่หน้าเว็บของ effbot กล่าวว่า (รายการไม่ได้รับการประเมินอีกต่อไป) เป็นสิ่งที่ไม่ถูกต้อง a
ได้รับการประเมินให้เป็นวัตถุของรายการถึงแม้ว่าตอนนี้เนื้อหาของวัตถุที่เป็น1 นี่คือผลของการส่งผ่านโดยอ้างอิง! ผลลัพธ์ของ foo (3) สามารถได้มาอย่างง่ายดายในลักษณะเดียวกัน
เพื่อตรวจสอบคำตอบของฉันเพิ่มเติมลองดูรหัสเพิ่มเติมสองรหัส
====== ครั้งที่ 2 ========
def foo(x, items=None):
if items is None:
items = []
items.append(x)
return items
foo(1) #return [1]
foo(2) #return [2]
foo(3) #return [3]
[]
เป็นวัตถุก็เช่นกันNone
(อดีตไม่เปลี่ยนแปลงในขณะที่หลังไม่เปลี่ยนรูป แต่ความสามารถในการเปลี่ยนแปลงไม่เกี่ยวข้องกับคำถาม) ไม่มีที่ไหนสักแห่งในอวกาศ แต่เรารู้ว่ามันอยู่ที่นั่นและมีสำเนาเดียวของ None ที่นั่น ดังนั้นทุกครั้งที่มีการเรียก foo รายการจะถูกประเมิน (ตรงข้ามกับคำตอบบางคำที่ได้รับการประเมินเพียงครั้งเดียว) เป็น None เพื่อให้ชัดเจนการอ้างอิง (หรือที่อยู่) ของ None จากนั้นใน foo รายการจะเปลี่ยนเป็น [] นั่นคือชี้ไปยังวัตถุอื่นที่มีที่อยู่ต่างกัน
====== ครั้งที่ 3 =======
def foo(x, items=[]):
items.append(x)
return items
foo(1) # returns [1]
foo(2,[]) # returns [2]
foo(3) # returns [1,3]
การเรียกใช้ foo (1) ทำให้รายการชี้ไปที่รายการวัตถุ [] โดยมีที่อยู่เช่น 11111111 เนื้อหาของรายการเปลี่ยนเป็น1ในฟังก์ชัน foo ในภาคต่อ แต่ที่อยู่จะไม่เปลี่ยนแปลง แต่ยังคงเป็น 11111111 . แล้ว foo (2, []) ก็ตามมา แม้ว่า [] ใน foo (2, []) จะมีเนื้อหาเหมือนกันกับพารามิเตอร์เริ่มต้น [] เมื่อเรียก foo (1) แต่ที่อยู่ก็ต่างกัน! เนื่องจากเราระบุพารามิเตอร์ไว้อย่างชัดเจนitems
จึงต้องใช้ที่อยู่ของใหม่นี้[]
กล่าวว่า 2222222 และส่งคืนหลังจากทำการเปลี่ยนแปลงบางอย่าง ตอนนี้ foo (3) ถูกดำเนินการ เนื่องจากx
มีให้เท่านั้นรายการจึงต้องใช้ค่าเริ่มต้นอีกครั้ง ค่าเริ่มต้นคืออะไร? มันถูกตั้งค่าเมื่อกำหนดฟังก์ชัน foo: วัตถุรายการที่อยู่ใน 11111111 ดังนั้นรายการจะถูกประเมินว่าเป็นที่อยู่ 11111111 โดยมีองค์ประกอบ 1 รายการที่อยู่ที่ 2222222 ยังมีองค์ประกอบหนึ่ง 2 ด้วย แต่ไม่ได้ชี้โดยรายการใด ๆ มากกว่า. ดังนั้นการผนวก 3 จะทำให้เป็นitems
[1,3]
จากคำอธิบายข้างต้นเราจะเห็นว่าหน้าเว็บEffbot ที่แนะนำในคำตอบที่ยอมรับนั้นไม่สามารถให้คำตอบที่เกี่ยวข้องกับคำถามนี้ได้ ยิ่งไปกว่านั้นฉันคิดว่าจุดหนึ่งในหน้าเว็บ Effbot นั้นไม่ถูกต้อง ฉันคิดว่ารหัสเกี่ยวกับ UI ปุ่มถูกต้อง:
for i in range(10):
def callback():
print "clicked button", i
UI.Button("button %s" % i, callback)
i
แต่ละปุ่มสามารถถือฟังก์ชั่นการโทรกลับที่แตกต่างกันซึ่งจะแสดงค่าที่แตกต่างกันของ ฉันสามารถยกตัวอย่างเพื่อแสดงสิ่งนี้:
x=[]
for i in range(10):
def callback():
print(i)
x.append(callback)
ถ้าเราดำเนินการx[7]()
เราจะได้ 7 ตามที่คาดไว้และx[9]()
จะให้ 9 ซึ่งเป็นค่าอื่นของi
.
TLDR: ค่าเริ่มต้นที่กำหนดเวลามีความสม่ำเสมอและแสดงออกได้ชัดเจนมากขึ้น
การกำหนดฟังก์ชันมีผลต่อขอบเขตสองขอบเขต: ขอบเขตการกำหนดที่มีฟังก์ชันและขอบเขตการดำเนินการที่อยู่ในฟังก์ชัน แม้ว่าจะค่อนข้างชัดเจนว่าบล็อกแมปกับขอบเขตอย่างไร แต่คำถามก็คือที่ที่def <name>(<args=defaults>):
เป็นของ:
... # defining scope
def name(parameter=default): # ???
... # execution scope
def name
ส่วนหนึ่งต้องประเมินผลในการกำหนดขอบเขต - เราต้องการที่name
จะสามารถใช้ได้มีหลังจากทั้งหมด การประเมินฟังก์ชันเฉพาะภายในตัวเองจะทำให้ไม่สามารถเข้าถึงได้
เนื่องจากparameter
เป็นชื่อที่คงที่เราจึงสามารถ "ประเมิน" พร้อมกันdef name
ได้ นอกจากนี้ยังมีความได้เปรียบมันผลิตฟังก์ชั่นที่มีลายเซ็นที่รู้จักกันในนามแทนการเปลือยname(parameter=...):
name(...):
ทีนี้จะประเมินเมื่อไหร่default
?
ความสอดคล้องกันแล้วว่า "ที่คำจำกัดความ": ทุกอย่างอื่น ๆdef <name>(<args=defaults>):
ได้รับการประเมินที่ดีที่สุดที่นิยามเช่นกัน การชะลอบางส่วนจะเป็นทางเลือกที่น่าประหลาดใจ
ทั้งสองทางเลือกไม่เทียบเท่ากัน: หากdefault
ได้รับการประเมินตามเวลานิยามก็ยังคงมีผลต่อเวลาดำเนินการ หากdefault
ได้รับการประเมิน ณ เวลาดำเนินการจะไม่มีผลกับเวลานิยาม การเลือก "ที่คำจำกัดความ" ทำให้สามารถแสดงทั้งสองกรณีได้ในขณะที่การเลือก "ที่ดำเนินการ" สามารถแสดงได้เพียงกรณีเดียว:
def name(parameter=defined): # set default at definition time
...
def name(parameter=default): # delay default until execution time
parameter = default if parameter is None else parameter
...
คำตอบอื่น ๆ ทั้งหมดอธิบายว่าเหตุใดสิ่งนี้จึงเป็นพฤติกรรมที่ดีและเป็นที่ต้องการหรือทำไมคุณไม่ควรต้องการสิ่งนี้ต่อไป ของฉันมีไว้สำหรับคนที่ดื้อรั้นที่ต้องการใช้สิทธิ์ในการงอภาษาให้เป็นไปตามที่ตนต้องการไม่ใช่ในทางอื่น
เราจะ "แก้ไข" พฤติกรรมนี้ด้วยมัณฑนากรที่จะคัดลอกค่าเริ่มต้นแทนที่จะใช้อินสแตนซ์เดิมซ้ำสำหรับอาร์กิวเมนต์ตำแหน่งแต่ละรายการที่เหลือไว้ที่ค่าเริ่มต้น
import inspect
from copy import copy
def sanify(function):
def wrapper(*a, **kw):
# store the default values
defaults = inspect.getargspec(function).defaults # for python2
# construct a new argument list
new_args = []
for i, arg in enumerate(defaults):
# allow passing positional arguments
if i in range(len(a)):
new_args.append(a[i])
else:
# copy the value
new_args.append(copy(arg))
return function(*new_args, **kw)
return wrapper
ตอนนี้เรามากำหนดฟังก์ชันของเราใหม่โดยใช้มัณฑนากรนี้:
@sanify
def foo(a=[]):
a.append(5)
return a
foo() # '[5]'
foo() # '[5]' -- as desired
โดยเฉพาะอย่างยิ่งสำหรับฟังก์ชันที่รับอาร์กิวเมนต์หลายรายการ เปรียบเทียบ:
# the 'correct' approach
def bar(a=None, b=None, c=None):
if a is None:
a = []
if b is None:
b = []
if c is None:
c = []
# finally do the actual work
ด้วย
# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
# wow, works right out of the box!
สิ่งสำคัญคือต้องทราบว่าโซลูชันข้างต้นจะหยุดทำงานหากคุณพยายามใช้คีย์เวิร์ด args ดังนี้:
foo(a=[4])
มัณฑนากรสามารถปรับเปลี่ยนเพื่อให้สามารถทำได้ แต่เราปล่อยไว้เป็นแบบฝึกหัดสำหรับผู้อ่าน;)