“ ความประหลาดใจน้อยที่สุด” และอาร์กิวเมนต์เริ่มต้นที่เปลี่ยนแปลงได้


2593

ทุกคนที่มี Python นานพอถูกกัด (หรือฉีกเป็นชิ้น ๆ ) โดยปัญหาต่อไปนี้:

def foo(a=[]):
    a.append(5)
    return a

[5]สามเณรงูใหญ่คาดว่าจะได้ฟังก์ชั่นนี้เสมอกลับรายการที่มีเพียงองค์ประกอบหนึ่ง: ผลลัพธ์จะแตกต่างกันมากและน่าประหลาดใจมาก (สำหรับมือใหม่):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

ผู้จัดการของฉันเคยพบกับคุณสมบัตินี้ครั้งแรกและเรียกมันว่า "ข้อบกพร่องในการออกแบบที่น่าทึ่ง" ของภาษา ฉันตอบว่าพฤติกรรมนั้นมีคำอธิบายพื้นฐานและเป็นเรื่องที่ทำให้งงงวยและคาดไม่ถึงถ้าคุณไม่เข้าใจ internals อย่างไรก็ตามฉันไม่สามารถตอบคำถามต่อไปนี้ (กับตัวเอง): อะไรคือเหตุผลในการเชื่อมอาร์กิวเมนต์เริ่มต้นที่คำจำกัดความของฟังก์ชั่นและไม่ทำงานในการดำเนินการ? ฉันสงสัยว่าพฤติกรรมที่มีประสบการณ์นั้นมีประโยชน์ในทางปฏิบัติ (ใครใช้ตัวแปรคงที่ใน 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บรรทัดจะเป็น "ลูกผสม" ในแง่ที่เป็นส่วนหนึ่งของการผูก วัตถุฟังก์ชั่น) จะเกิดขึ้นที่คำจำกัดความและส่วนหนึ่ง (การกำหนดพารามิเตอร์เริ่มต้น) ที่เวลาเรียกใช้ฟังก์ชั่น

พฤติกรรมที่เกิดขึ้นจริงนั้นสอดคล้องกันมากขึ้น: ทุกอย่างในบรรทัดนั้นจะถูกประเมินเมื่อประมวลผลบรรทัดนั้นซึ่งหมายถึงนิยามของฟังก์ชัน



4
ฉันไม่สงสัยเลยว่าข้อโต้แย้งที่เปลี่ยนแปลงไม่ได้ละเมิดหลักการความประหลาดใจน้อยที่สุดสำหรับคนทั่วไปและฉันได้เห็นผู้เริ่มต้นก้าวไปที่นั่นจากนั้นจึงแทนที่รายการส่งเมลด้วยรายการส่งจดหมาย อย่างไรก็ตามข้อโต้แย้งที่ไม่แน่นอนยังคงอยู่ในแนวเดียวกันกับ Python Zen (Pep 20) และอยู่ในประโยค "ชัดเจนสำหรับชาวดัตช์" (เข้าใจ / ใช้ประโยชน์จากโปรแกรมเมอร์งูหลามคอร์) วิธีแก้ปัญหาที่แนะนำด้วยสตริง doc เป็นวิธีที่ดีที่สุด แต่การต่อต้านสตริง doc และเอกสารใด ๆ ที่เขียนนั้นไม่ใช่เรื่องแปลกในปัจจุบัน โดยส่วนตัวฉันชอบมัณฑนากร (พูด @fixed_defaults)
Serge

5
ข้อโต้แย้งของฉันเมื่อฉันเจอสิ่งนี้คือ: "ทำไมคุณต้องสร้างฟังก์ชั่นที่ส่งกลับค่าความไม่แน่นอนที่อาจเลือกให้เป็นความไม่แน่นอนที่คุณจะส่งผ่านไปยังฟังก์ชั่นได้หรือไม่? ทำทั้งสองอย่างด้วยฟังก์ชั่นเดียวและทำไมต้องมีการเขียนล่ามใหม่เพื่อให้คุณทำเช่นนั้นได้โดยไม่ต้องเพิ่มสามบรรทัดในรหัสของคุณ " เนื่องจากเรากำลังพูดถึงการเขียนใหม่ในวิธีที่ล่ามจัดการคำจำกัดความของฟังก์ชั่นและการอพยพที่นี่ นั่นเป็นเรื่องที่ต้องทำสำหรับกรณีการใช้งานที่แทบจะไม่จำเป็น
Alan Leuthard

12
"ผู้เริ่มต้นหลามคาดหวังว่าฟังก์ชันนี้จะส่งคืนรายการที่มีองค์ประกอบเดียวเท่านั้น: [5]" ฉันเป็นสามเณรงูหลามและฉันจะไม่คาดหวังนี้เพราะเห็นได้ชัดว่าfoo([1])จะกลับมาไม่ได้[1, 5] [5]สิ่งที่คุณหมายถึงการบอกก็คือว่าเป็นสามเณรคาดว่าจะได้ฟังก์ชั่นเรียกว่ามีพารามิเตอร์ไม่มี[5]มักจะกลับ
symplectomorphic

2
คำถามนี้ถามว่า"ทำไม [วิธีที่ผิด] จึงถูกนำมาใช้เพื่อ?" ไม่ถามว่า "ทางที่ถูกต้องคืออะไร" ซึ่งครอบคลุมโดย [ ทำไมจึงใช้ arg = None แก้ไขปัญหาอาร์กิวเมนต์เริ่มต้นที่ไม่แน่นอนของ Python ] * ( stackoverflow.com/questions/10676729/… ) ผู้ใช้ใหม่มักให้ความสนใจน้อยลงในอดีตและอื่น ๆ อีกมากมายดังนั้นบางครั้งจึงเป็นลิงก์ / ล่อที่มีประโยชน์มากในการอ้างอิง
smci

คำตอบ:


1612

จริงๆแล้วนี่ไม่ใช่ข้อบกพร่องด้านการออกแบบและไม่ใช่เพราะเรื่องภายในหรือประสิทธิภาพ
มันมาจากความจริงที่ว่าฟังก์ชั่นใน Python เป็นวัตถุชั้นหนึ่งและไม่เพียง แต่ส่วนหนึ่งของรหัส

ทันทีที่คุณคิดอย่างนี้มันก็สมเหตุสมผลแล้ว: ฟังก์ชั่นคือวัตถุที่ถูกประเมินตามคำนิยามของมัน พารามิเตอร์เริ่มต้นเป็นชนิดของ "ข้อมูลสมาชิก" ดังนั้นสถานะของพวกเขาอาจเปลี่ยนจากการโทรหนึ่งไปยังอีกสายหนึ่ง - เหมือนกับในวัตถุอื่น ๆ

ในกรณีใด ๆ Effbot มีคำอธิบายที่ดีมากในเหตุผลสำหรับพฤติกรรมนี้ในการเริ่มต้นค่าพารามิเตอร์ในหลาม
ฉันพบว่ามันชัดเจนมากและฉันขอแนะนำให้อ่านเพื่อความรู้ที่ดีขึ้นเกี่ยวกับการทำงานของวัตถุ


80
สำหรับทุกคนที่อ่านคำตอบข้างต้นฉันขอแนะนำให้คุณสละเวลาอ่านบทความ Effbot ที่เชื่อมโยง เช่นเดียวกับข้อมูลที่เป็นประโยชน์อื่น ๆ ส่วนหนึ่งเกี่ยวกับวิธีการใช้คุณสมบัติภาษานี้สำหรับการแคชผล / การจำได้นั้นมีประโยชน์มากที่จะรู้!
Cam Jackson

85
แม้ว่ามันจะเป็นวัตถุชั้นหนึ่ง แต่ก็ยังอาจจินตนาการถึงการออกแบบที่รหัสสำหรับแต่ละค่าเริ่มต้นจะถูกเก็บไว้พร้อมกับวัตถุและประเมินอีกครั้งในแต่ละครั้งที่มีการเรียกใช้ฟังก์ชัน ฉันไม่ได้บอกว่ามันจะดีกว่าฟังก์ชั่นที่เป็นวัตถุชั้นหนึ่งไม่ได้ขัดขวางอย่างเต็มที่
gerrit

312
ขออภัย แต่สิ่งที่ถือว่าเป็น "การ WTF ที่ใหญ่ที่สุดในงูใหญ่" เป็นส่วนใหญ่แน่นอนข้อบกพร่องการออกแบบ นี่คือแหล่งที่มาของข้อบกพร่องสำหรับทุกคนในบางจุดเพราะไม่มีใครคาดหวังว่าพฤติกรรมในตอนแรก - ซึ่งหมายความว่ามันไม่ควรได้รับการออกแบบวิธีการเริ่มต้นด้วย ฉันไม่สนใจว่าจะต้องห่วงอะไรพวกเขาควรออกแบบ Python เพื่อให้อาร์กิวเมนต์เริ่มต้นไม่คงที่
BlueRaja - Danny Pflughoeft

192
ไม่ว่าจะเป็นข้อบกพร่องในการออกแบบหรือไม่คำตอบของคุณดูเหมือนจะบอกเป็นนัยว่าพฤติกรรมนี้มีความจำเป็นอย่างใดเป็นธรรมชาติและชัดเจนเนื่องจากหน้าที่เป็นวัตถุชั้นหนึ่งและนั่นไม่ใช่กรณี Python มีการปิด หากคุณแทนที่อาร์กิวเมนต์เริ่มต้นด้วยการกำหนดค่าในบรรทัดแรกของฟังก์ชั่นมันจะประเมินการแสดงออกของการโทรแต่ละครั้ง (อาจใช้ชื่อประกาศในขอบเขตล้อมรอบ) ไม่มีเหตุผลเลยที่มันจะไม่มีความเป็นไปได้หรือเหตุผลที่จะประเมินข้อโต้แย้งเริ่มต้นทุกครั้งที่มีการเรียกใช้ฟังก์ชันในลักษณะเดียวกัน
Mark Amery

24
functions are objectsการออกแบบที่ไม่ตรงตามมาจาก ในกระบวนทัศน์ของคุณข้อเสนอนี้จะนำค่าเริ่มต้นของฟังก์ชันมาใช้เป็นคุณสมบัติแทนที่จะเป็นคุณสมบัติ
bukzor

273

สมมติว่าคุณมีรหัสต่อไปนี้

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

เมื่อฉันเห็นคำแถลงการณ์เกี่ยวกับการกินสิ่งที่น่าประหลาดใจอย่างน้อยที่สุดก็คือการคิดว่าถ้าไม่ได้ระบุพารามิเตอร์ตัวแรกมันจะเท่ากับ tuple ("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: อาร์กิวเมนต์ของฟังก์ชันเริ่มต้นจะถูกประเมินเมื่อมีการกำหนดฟังก์ชันและวัตถุนั้นจะเป็นค่าเริ่มต้นเสมอ ฉันคิดว่าพวกเขาสามารถใช้กรณีพิเศษโดยใช้รายการที่ว่างเปล่า แต่การห่อแบบพิเศษนั้นจะทำให้เกิดความประหลาดใจมากยิ่งขึ้น


30
ฉันคิดว่ามันเป็นเรื่องของการอภิปราย คุณกำลังทำกับตัวแปรทั่วโลก การประเมินใด ๆ ที่ทำในรหัสของคุณที่เกี่ยวข้องกับตัวแปรทั่วโลกของคุณในตอนนี้ (ถูกต้อง) หมายถึง ("บลูเบอร์รี่", "มะม่วง") พารามิเตอร์เริ่มต้นอาจเป็นเหมือนกรณีอื่น ๆ
Stefano Borini

47
จริงๆแล้วฉันไม่คิดว่าฉันเห็นด้วยกับตัวอย่างแรกของคุณ ผมไม่แน่ใจว่าผมชอบความคิดของการปรับเปลี่ยนการเริ่มต้นเช่นเดียวกับที่อยู่ในสถานที่แรก แต่ถ้าผมทำผมคาดหวังว่ามันจะทำงานตรงตามที่คุณอธิบาย - ("blueberries", "mangos")การเปลี่ยนค่าเริ่มต้น
Ben Blank

12
พารามิเตอร์เริ่มต้นเป็นเหมือนกรณีอื่น ๆ สิ่งที่ไม่คาดคิดก็คือพารามิเตอร์เป็นตัวแปรโกลบอลไม่ใช่ตัวแปรท้องถิ่น ซึ่งจะเป็นเพราะรหัสจะถูกดำเนินการตามคำนิยามฟังก์ชั่นไม่โทร เมื่อคุณได้รับสิ่งนั้นแล้วสิ่งนั้นก็เหมือนกันสำหรับชั้นเรียนมันชัดเจนอย่างสมบูรณ์
Lennart Regebro

17
ฉันพบว่าตัวอย่างทำให้เข้าใจผิดมากกว่าความฉลาด หากsome_random_function()ผนวกกับfruitsแทนที่จะกำหนดให้มันeat() จะเปลี่ยนพฤติกรรมของ มากสำหรับการออกแบบที่ยอดเยี่ยมในปัจจุบัน หากคุณใช้อาร์กิวเมนต์เริ่มต้นที่มีการอ้างอิงที่อื่นแล้วปรับเปลี่ยนการอ้างอิงจากภายนอกฟังก์ชั่นคุณจะถามปัญหา WTF ที่แท้จริงคือเมื่อผู้ใช้กำหนดอาร์กิวเมนต์เริ่มต้นใหม่ (รายการตามตัวอักษรหรือการเรียกไปยังตัวสร้าง) และยังได้รับบิต
alexis

13
คุณเพิ่งประกาศอย่างชัดเจนglobalและมอบหมาย tuple ใหม่ - ไม่มีอะไรน่าประหลาดใจหากeatทำงานแตกต่างหลังจากนั้น
user3467349

241

ส่วนที่เกี่ยวข้องของเอกสาร :

ค่าพารามิเตอร์เริ่มต้นจะถูกประเมินจากซ้ายไปขวาเมื่อมีการดำเนินการกำหนดฟังก์ชัน นี่หมายความว่านิพจน์ได้รับการประเมินหนึ่งครั้งเมื่อมีการกำหนดฟังก์ชันและใช้ค่า“ คำนวณล่วงหน้า” เดียวกันสำหรับการโทรแต่ละครั้ง สิ่งนี้สำคัญอย่างยิ่งที่จะต้องเข้าใจเมื่อพารามิเตอร์เริ่มต้นเป็นวัตถุที่ไม่แน่นอนเช่นรายการหรือพจนานุกรม: หากฟังก์ชันแก้ไขวัตถุ (เช่นการต่อท้ายรายการในรายการ) ค่าเริ่มต้นจะได้รับการแก้ไข โดยทั่วไปนี่ไม่ใช่สิ่งที่ตั้งใจไว้ วิธีนี้คือการใช้Noneเป็นค่าเริ่มต้นและทดสอบอย่างชัดเจนในเนื้อหาของฟังก์ชันเช่น:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
วลี "นี่ไม่ใช่สิ่งที่ตั้งใจ" และ "วิธีแก้ไขคือ" กลิ่นเหมือนพวกเขากำลังบันทึกข้อบกพร่องในการออกแบบ
bukzor

4
@ Matewew: ฉันตระหนักดี แต่ก็ไม่คุ้มกับหลุมพราง โดยทั่วไปแล้วคุณจะเห็นคำแนะนำรูปแบบและเส้นขอบโดยไม่มีเงื่อนไขกำหนดค่าเริ่มต้นที่ไม่แน่นอนว่าผิดด้วยเหตุผลนี้ วิธีที่ชัดเจนในการทำสิ่งเดียวกันคือการใส่แอตทริบิวต์ลงในฟังก์ชัน ( function.data = []) หรือดีกว่าทำวัตถุ
bukzor

6
@bukzor: ข้อผิดพลาดจะต้องมีการบันทึกและจัดทำเอกสารซึ่งเป็นสาเหตุที่คำถามนี้ดีและได้รับ upvotes มากมาย ในเวลาเดียวกันข้อผิดพลาดไม่จำเป็นต้องถูกลบออก มีผู้เริ่มต้น Python กี่คนที่ผ่านรายการไปยังฟังก์ชั่นที่ได้ทำการแก้ไขและรู้สึกตกใจเมื่อเห็นการเปลี่ยนแปลงที่ปรากฏในตัวแปรดั้งเดิม เมื่อคุณเข้าใจวิธีการใช้วัตถุเหล่านั้น ฉันเดาว่ามันเป็นเพียงแค่ความเห็นที่ผิดพลาดในเรื่องนี้
Matthew

33
วลี "นี่ไม่ใช่สิ่งที่ตั้งใจ" หมายถึง "ไม่ใช่สิ่งที่โปรแกรมเมอร์อยากจะเกิดขึ้นจริง ๆ " ไม่ใช่ "ไม่ใช่สิ่งที่หลามควรจะทำ"
holdenweb

4
@ โฮลเด็นว้าวฉันไปงานเลี้ยงช้า เมื่อพิจารณาถึงบริบท bukzor นั้นถูกต้องสมบูรณ์: พวกเขากำลังบันทึกพฤติกรรม / ผลที่ไม่ "ตั้งใจ" เมื่อพวกเขาตัดสินใจว่าภาษาควรดำเนินการตามคำจำกัดความของฟังก์ชั่น เนื่องจากเป็นผลมาจากการออกแบบที่ไม่ได้ตั้งใจทำให้เป็นข้อบกพร่องในการออกแบบ หากไม่ใช่ข้อบกพร่องด้านการออกแบบก็ไม่จำเป็นต้องเสนอ "วิธีแก้ไข"
code_dredd

118

ฉันไม่รู้อะไรเลยเกี่ยวกับผลงานภายในของ Python interpreter (และฉันก็ไม่ใช่ผู้เชี่ยวชาญในการแปลและล่ามด้วย) ดังนั้นอย่าโทษฉันถ้าฉันเสนออะไรที่ไม่ถูกต้องหรือเป็นไปไม่ได้

โดยมีเงื่อนไขว่าวัตถุหลามไม่แน่นอนฉันคิดว่าสิ่งนี้ควรนำมาพิจารณาเมื่อออกแบบสิ่งเริ่มต้นอาร์กิวเมนต์ เมื่อคุณยกตัวอย่างรายการ:

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):

11
คุณสามารถทำได้: def x (a = None): แล้วถ้า a เป็น None ให้ตั้งค่า a = datetime.datetime.now ()
Anon

20
ขอบคุณสำหรับสิ่งนี้. ฉันไม่สามารถวางนิ้วลงบนสาเหตุที่ทำให้ฉันหงุดหงิดไม่สิ้นสุด คุณทำมันได้อย่างสวยงามโดยมีฝอยและความสับสนขั้นต่ำ ในขณะที่มีคนมาจากการเขียนโปรแกรมระบบใน C ++ และบางครั้งไร้เดียงสา "แปล" คุณสมบัติภาษาเพื่อนเท็จคนนี้เตะฉันเข้ามาในหัวอันยิ่งใหญ่ครั้งใหญ่เช่นเดียวกับคุณลักษณะของคลาส ฉันเข้าใจว่าทำไมสิ่งต่าง ๆ ถึงเป็นแบบนี้ แต่ฉันก็อดไม่ได้ที่จะไม่ชอบไม่ว่าจะเกิดอะไรขึ้น อย่างน้อยก็ตรงกันข้ามกับประสบการณ์ของฉันที่ฉันอาจจะ (หวังว่า) จะไม่ลืมมัน ...
AndreasT

5
@Andreas เมื่อคุณใช้ Python นานพอคุณจะเริ่มเห็นว่าตรรกะนั้นเป็นอย่างไรสำหรับ Python ในการตีความสิ่งต่าง ๆ ตามคุณลักษณะของคลาส - เนื่องจากเป็นลักษณะนิสัยและข้อ จำกัด ของภาษาเช่น C ++ (และ Java และ C # ... ) มันทำให้รู้สึกถึงเนื้อหาของclass {}บล็อกที่จะตีความว่าเป็นของกรณี :) แต่เมื่อชั้นเรียนเป็นวัตถุชั้นหนึ่งเห็นได้ชัดว่าสิ่งที่เป็นธรรมชาติสำหรับเนื้อหา (ในหน่วยความจำ) เพื่อสะท้อนเนื้อหาของพวกเขา (ในรหัส)
Karl Knechtel

6
โครงสร้างเชิงบรรทัดฐานไม่มีการเล่นโวหารหรือข้อ จำกัด ในหนังสือของฉัน ฉันรู้ว่ามันอาจจะเงอะงะและน่าเกลียด แต่คุณสามารถเรียกมันว่า "คำจำกัดความ" ของบางสิ่งบางอย่าง ภาษาแบบไดนามิกดูเหมือนว่าผู้นิยมอนาธิปไตยสำหรับฉัน: แน่นอนว่าทุกคนมีอิสระ แต่คุณต้องมีโครงสร้างเพื่อให้มีคนล้างถังขยะและปูถนน เดาว่าฉันแก่แล้ว ... :)
AndreasT

4
คำจำกัดความของฟังก์ชั่นถูกเรียกใช้งานที่เวลาโหลดโมดูล ร่างกายของฟังก์ชั่นจะดำเนินการในเวลาที่ฟังก์ชั่นการโทร อาร์กิวเมนต์เริ่มต้นเป็นส่วนหนึ่งของการกำหนดฟังก์ชั่นไม่ใช่ของร่างกายฟังก์ชั่น (มันซับซ้อนมากขึ้นสำหรับฟังก์ชั่นซ้อนกัน)
Lutz Prechelt

84

เหตุผลก็คือค่อนข้างง่ายที่จะทำการผูกเมื่อรันโค้ดและคำจำกัดความของฟังก์ชั่นถูกดำเนินการดี ... เมื่อฟังก์ชั่นถูกกำหนด

เปรียบเทียบสิ่งนี้:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

รหัสนี้ทนทุกข์ทรมานจากเหตุการณ์ที่ไม่คาดคิดที่เหมือนกัน bananas เป็นแอททริบิวต์คลาสดังนั้นเมื่อคุณเพิ่มรายการเข้าไปแอปจะถูกเพิ่มไปยังอินสแตนซ์ทั้งหมดของคลาสนั้น เหตุผลนั้นเหมือนกันหมด

มันเป็นแค่ "มันทำงานอย่างไร" และทำให้มันทำงานแตกต่างกันในกรณีของฟังก์ชั่นอาจจะซับซ้อนและในกรณีของคลาสอาจเป็นไปไม่ได้หรืออย่างน้อยก็ทำให้วัตถุ instantiation ช้าลงมากอย่างที่คุณจะต้องเก็บรหัสคลาสไว้ และดำเนินการเมื่อมีการสร้างวัตถุ

ใช่มันไม่คาดคิด แต่เมื่อเงินลดลงมันเข้ากันได้อย่างสมบูรณ์แบบกับวิธีการทำงานของ Python โดยทั่วไป ในความเป็นจริงมันเป็นเครื่องช่วยสอนที่ดีและเมื่อคุณเข้าใจว่าทำไมสิ่งนี้ถึงเกิดขึ้น

ที่กล่าวว่ามันควรมีคุณสมบัติเด่นในการสอน Python ที่ดี เพราะอย่างที่คุณพูดถึงทุกคนประสบปัญหานี้ไม่ช้าก็เร็ว


คุณจะกำหนดคุณลักษณะคลาสที่แตกต่างกันสำหรับแต่ละอินสแตนซ์ของคลาสได้อย่างไร
Kieveli

19
หากมันแตกต่างกันไปในแต่ละครั้งมันไม่ใช่แอตทริบิวต์ของคลาส แอตทริบิวต์ class เป็นคุณสมบัติบน CLASS ดังนั้นชื่อ ดังนั้นจึงเหมือนกันสำหรับทุกกรณี
Lennart Regebro

1
คุณกำหนดคุณลักษณะในคลาสที่แตกต่างกันสำหรับแต่ละอินสแตนซ์ของคลาสได้อย่างไร (นิยามอีกครั้งสำหรับผู้ที่ไม่สามารถตัดสินได้ว่าคนที่ไม่คุ้นเคยกับการประชุมการตั้งชื่อของงูใหญ่อาจถามเกี่ยวกับตัวแปรสมาชิกปกติของชั้นเรียน)
Kieveli

@Kievieli: คุณกำลังพูดถึงตัวแปรสมาชิกปกติของคลาส :-) คุณกำหนดแอตทริบิวต์ของอินสแตนซ์โดยบอกว่า self.attribute = value ในวิธีการใด ๆ ตัวอย่างเช่น __init __ ()
Lennart Regebro

@Kieveli: สองคำตอบ: คุณทำไม่ได้เพราะสิ่งใดก็ตามที่คุณกำหนดในระดับชั้นจะเป็นแอตทริบิวต์ class และอินสแตนซ์ใด ๆ ที่เข้าถึงแอตทริบิวต์นั้นจะเข้าถึงแอตทริบิวต์ class เดียวกัน คุณสามารถ, / sort ของ /, โดยใช้propertys - ซึ่งเป็นฟังก์ชันระดับคลาสที่ทำหน้าที่เหมือนแอ็ตทริบิวต์ปกติ แต่บันทึกแอ็ตทริบิวต์ในอินสแตนซ์แทนคลาส (โดยใช้self.attribute = valueเป็น Lennart กล่าว)
Ethan Furman

66

ทำไมคุณไม่ใคร่ครวญ

ฉันประหลาดใจจริง ๆไม่มีใครได้ทำการวิปัสสนาที่ลึกซึ้งที่เสนอโดย 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
เป็นid(...)สิ่งที่จำเป็นสำหรับการตรวจสอบที่ผ่านมาหรือจะisดำเนินการตอบคำถามเดียวกันได้หรือไม่
das-g

1
@ das-g isทำได้ดีฉันเพิ่งใช้id(val)เพราะฉันคิดว่ามันอาจใช้งานง่ายกว่า
Dimitris Fasarakis Hilliard

การใช้Noneเป็นค่าเริ่มต้นจะ จำกัด ประโยชน์ของการ__defaults__วิปัสสนาอย่างรุนแรงดังนั้นฉันจึงไม่คิดว่ามันจะทำงานได้ดีเท่ากับการป้องกันการมี__defaults__วิธีการทำงาน การประเมินที่ขี้เกียจจะทำมากขึ้นเพื่อให้ค่าเริ่มต้นของฟังก์ชั่นมีประโยชน์จากทั้งสองด้าน
Brilliand

58

ฉันเคยคิดว่าการสร้างวัตถุที่รันไทม์จะเป็นวิธีที่ดีกว่า ฉันไม่แน่ใจตอนนี้เนื่องจากคุณสูญเสียคุณสมบัติที่มีประโยชน์บางอย่างแม้ว่ามันอาจจะคุ้มค่าโดยไม่คำนึงถึงเพียงเพื่อป้องกันความสับสนของมือใหม่ ข้อเสียของการทำเช่นนี้คือ:

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:

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, []))

ข้อมูลนี้มีประโยชน์มากสำหรับสิ่งต่าง ๆ เช่นการสร้างเอกสาร metaprogramming มัณฑนากร ฯลฯ

ทีนี้สมมติว่าพฤติกรรมของค่าเริ่มต้นสามารถเปลี่ยนแปลงได้ดังนั้นสิ่งนี้จึงเท่ากับ:

_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=[]

อย่างไรก็ตามเราสูญเสียความสามารถในการไตร่ตรองและดูว่าอาร์กิวเมนต์เริ่มต้นคืออะไร เนื่องจากวัตถุไม่ได้ถูกสร้างขึ้นเราจึงไม่สามารถรับสิ่งเหล่านี้ได้โดยไม่เรียกใช้ฟังก์ชัน สิ่งที่ดีที่สุดที่เราสามารถทำได้คือการเก็บรหัสแหล่งที่มาและส่งคืนเป็นสตริง


1
คุณสามารถบรรลุวิปัสสนาได้ถ้าสำหรับแต่ละฟังก์ชั่นในการสร้างอาร์กิวเมนต์เริ่มต้นแทนที่จะเป็นค่า โมดูลตรวจสอบจะเรียกใช้ฟังก์ชันนั้น
yairchu

@SilentGhost: ฉันกำลังพูดถึงว่าพฤติกรรมเปลี่ยนไปเพื่อสร้างมันขึ้นมาใหม่หรือไม่ - การสร้างมันขึ้นมาครั้งหนึ่งเป็นพฤติกรรมปัจจุบันและทำไมปัญหาเริ่มต้นที่ไม่แน่นอน
Brian

1
@yairchu: ถือว่าการก่อสร้างมีความปลอดภัย (เช่นไม่มีผลข้างเคียง) การสำรวจอย่างระมัดระวังไม่ควรทำอะไร แต่การประเมินโค้ดโดยพลการอาจมีผลกระทบได้
Brian

1
การออกแบบภาษาที่แตกต่างมักจะหมายถึงการเขียนสิ่งที่แตกต่าง ตัวอย่างแรกของคุณสามารถเขียนได้ง่ายเช่น: _ แพง = แพง (); def foo (หาเรื่อง = _ ราคาถูก) หากคุณไม่ต้องการให้ประเมินใหม่
Glenn Maynard

@ Glenn - นั่นคือสิ่งที่ฉันหมายถึงด้วย "แคชตัวแปรภายนอก" - มันเป็น verbose อีกเล็กน้อยและคุณท้ายด้วยตัวแปรพิเศษใน namespace ของคุณว่า
46916 Brian Brian

55

5 คะแนนในการป้องกันของ Python

  1. ความเรียบง่าย : พฤติกรรมนั้นง่ายในแง่ต่อไปนี้: คนส่วนใหญ่ตกหลุมพรางนี้เพียงครั้งเดียวไม่ใช่หลายครั้ง

  2. ความสอดคล้อง : Python ส่งผ่านวัตถุเสมอไม่ใช่ชื่อ พารามิเตอร์เริ่มต้นคือส่วนหนึ่งของส่วนหัวของฟังก์ชั่น (ไม่ใช่ส่วนของฟังก์ชั่น) ดังนั้นจึงควรได้รับการประเมิน ณ เวลาโหลดโมดูล (และเฉพาะที่เวลาโหลดโมดูลยกเว้นอยู่ซ้อนกัน) ไม่ใช่เวลาเรียกใช้ฟังก์ชัน

  3. ประโยชน์ : ตามที่ Frederik Lundh ชี้ให้เห็นในคำอธิบายของเขาว่า "ค่าพารามิเตอร์เริ่มต้นใน Python"พฤติกรรมปัจจุบันอาจมีประโยชน์สำหรับการตั้งโปรแกรมขั้นสูง (ใช้เท่าที่จำเป็น)

  4. เอกสารเพียงพอ : ในเอกสารหลามพื้นฐานที่สุด, กวดวิชาที่ปัญหาจะมีการประกาศเสียงดังเป็น"คำเตือนที่สำคัญ"ในครั้งแรกส่วนย่อยของมาตรา "เพิ่มเติมเกี่ยวกับการกำหนดฟังก์ชั่น" คำเตือนยังใช้ตัวหนาซึ่งไม่ค่อยได้ใช้นอกส่วนหัว RTFM: อ่านคู่มืออย่างละเอียด

  5. การเรียนรู้เมตา : การตกหลุมพรางนั้นเป็นช่วงเวลาที่มีประโยชน์มาก (อย่างน้อยถ้าคุณเป็นผู้เรียนไตร่ตรอง) เพราะคุณจะเข้าใจจุด "ความสอดคล้อง" ด้านบนได้ดีขึ้นและนั่นจะสอนคุณเกี่ยวกับงูหลาม


18
ฉันใช้เวลาหนึ่งปีในการค้นหาพฤติกรรมนี้ทำให้รหัสของฉันยุ่งเกี่ยวกับการผลิตลงเอยด้วยการลบคุณลักษณะที่สมบูรณ์จนกว่าฉันจะชนกับข้อบกพร่องในการออกแบบนี้โดยบังเอิญ ฉันใช้ Django เนื่องจากสภาวะแวดล้อม staging ไม่มีคำร้องขอจำนวนมากข้อผิดพลาดนี้จึงไม่มีผลกระทบใด ๆ กับ QA เมื่อเราเริ่มใช้งานและได้รับการร้องขอพร้อมกันจำนวนมาก - ฟังก์ชั่นยูทิลิตี้บางอย่างเริ่มเขียนทับพารามิเตอร์ของกันและกัน! ทำให้รูโหว่ความปลอดภัยและสิ่งอื่น ๆ ไม่ได้
oriadam

7
@Iadadam ไม่มีความผิด แต่ฉันสงสัยว่าคุณเรียน Python ได้อย่างไรโดยไม่เคยทำมาก่อน ฉันเพิ่งจะเรียนรู้ Python ในตอนนี้และสิ่งที่เป็นอันตรายนี้ถูกกล่าวถึงในการสอน Python อย่างเป็นทางการข้างๆการกล่าวถึงข้อโต้แย้งแรก (ดังที่กล่าวไว้ในข้อ 4 ของคำตอบนี้) ฉันคิดว่าคุณธรรมคือ - ค่อนข้างไม่เห็นใจ - เพื่ออ่านเอกสารอย่างเป็นทางการของภาษาที่คุณใช้ในการสร้างซอฟต์แวร์ที่ใช้งานจริง
สัญลักษณ์แทน

นอกจากนี้มันจะน่าแปลกใจ (สำหรับฉัน) ถ้าฟังก์ชั่นที่มีความซับซ้อนที่ไม่รู้จักถูกเรียกใช้นอกเหนือจากการเรียกฟังก์ชันที่ฉันกำลังทำอยู่
Vatine

52

พฤติกรรมนี้อธิบายได้ง่ายโดย:

  1. ประกาศฟังก์ชั่น (ชั้น ฯลฯ ) จะดำเนินการเพียงครั้งเดียวการสร้างวัตถุค่าเริ่มต้นทั้งหมด
  2. ทุกอย่างถูกส่งผ่านโดยการอ้างอิง

ดังนั้น:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a ไม่เปลี่ยนแปลง - ทุกการเรียกการมอบหมายจะสร้างวัตถุ int ใหม่ - วัตถุใหม่จะถูกพิมพ์
  2. b ไม่เปลี่ยนแปลง - อาร์เรย์ใหม่สร้างจากค่าเริ่มต้นและพิมพ์
  3. c การเปลี่ยนแปลง - การดำเนินการจะดำเนินการกับวัตถุเดียวกัน - และมันจะถูกพิมพ์

(ที่จริงแล้วการเพิ่มเป็นตัวอย่างที่ไม่ดี แต่จำนวนเต็มที่ยังไม่เปลี่ยนรูปยังคงเป็นประเด็นหลักของฉัน)
Anon

ตระหนักถึงความผิดหวังของฉันหลังจากตรวจสอบเพื่อดูว่าด้วยการตั้งค่า b เป็น [], b .__ เพิ่ม __ ([1]) ส่งคืน [1] แต่ยังทิ้ง b ยัง [] แม้ว่ารายการจะไม่แน่นอน ความผิดฉันเอง.
Anon

@ อานนท์: มี__iadd__แต่มันไม่ทำงานกับ int แน่นอน. :-)
Veky

35

สิ่งที่คุณถามคือเหตุผลนี้:

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 อย่างชัดเจน (ไม่มี, ไม่มี) ซึ่งเราจะไม่สนใจ

กล่าวอีกนัยหนึ่งแทนที่จะประเมินพารามิเตอร์เริ่มต้นทำไมไม่เก็บแต่ละพารามิเตอร์ไว้และประเมินค่าเมื่อเรียกใช้ฟังก์ชัน

คำตอบเดียวน่าจะอยู่ตรงนั้น - มันจะเปลี่ยนทุกฟังก์ชั่นด้วยพารามิเตอร์เริ่มต้นให้เป็นการปิด แม้ว่ามันจะถูกซ่อนอยู่ในล่ามและไม่ใช่การปิดเต็มรูปแบบ แต่ข้อมูลก็จะถูกเก็บไว้ที่ใดที่หนึ่ง มันจะช้ากว่าและใช้หน่วยความจำมากขึ้น


6
มันไม่จำเป็นต้องเป็นการปิด - วิธีที่ดีกว่าที่จะคิดว่ามันจะทำให้ bytecode สร้างค่าเริ่มต้นของบรรทัดแรกของรหัส - หลังจากทั้งหมดที่คุณรวบรวมร่างกาย ณ จุดนั้น - ไม่มีความแตกต่างระหว่างรหัสจริง ในข้อโต้แย้งและรหัสในร่างกาย
Brian

10
จริง แต่มันจะยังคงทำให้ Python ช้าลงและมันจะค่อนข้างน่าประหลาดใจเว้นแต่ว่าคุณจะทำเช่นเดียวกันสำหรับการกำหนดคลาส ชั้น ดังกล่าวการแก้ไขจะน่าแปลกใจกว่าปัญหา
Lennart Regebro

ตกลงกับ Lennart ในขณะที่กุยโดชอบที่จะพูดว่าสำหรับทุก ๆ ภาษาหรือห้องสมุดมาตรฐานมีบางคนที่ใช้มัน
เจสันเบเกอร์

6
การเปลี่ยนตอนนี้จะกลายเป็นความวิกลจริต - เราแค่สำรวจว่าทำไมมันถึงเป็นแบบนั้น หากการประเมินเริ่มต้นช้าเกินไปก็ไม่น่าแปลกใจเลย มันเป็นความจริงอย่างแน่นอนที่แกนกลางที่แตกต่างของการแยกวิเคราะห์จะมีการกวาดและอาจมีผลกระทบที่คลุมเครือจำนวนมากเกี่ยวกับภาษาโดยรวม
Glenn Maynard

35

1) ปัญหาที่เรียกว่า "อาร์กิวเมนต์เริ่มต้นที่ไม่แน่นอน" เป็นตัวอย่างพิเศษที่แสดงให้เห็นว่า:
"ฟังก์ชั่นทั้งหมดที่มีปัญหานี้ประสบจากปัญหาผลข้างเคียงที่คล้ายกันกับพารามิเตอร์จริง "
ซึ่งขัดกับกฎของการเขียนโปรแกรมการทำงาน มักจะมองไม่เห็นและควรได้รับการแก้ไขทั้งสองอย่างพร้อมกัน

ตัวอย่าง:

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คือแอตทริบิวต์ส่วนตัว)

สรุป:
วัตถุอินพุตพารามิเตอร์ไม่ควรแก้ไขในตำแหน่ง (กลายพันธุ์) และไม่ควรผูกเข้ากับวัตถุที่ส่งคืนโดยฟังก์ชัน (หากเราทำการเขียนโปรแกรมล่วงหน้าโดยไม่มีผลข้างเคียงซึ่งขอแนะนำอย่างยิ่งให้ดูที่วิกิเกี่ยวกับ "ผลข้างเคียง" (สองย่อหน้าแรกมีความเกี่ยวข้องในบริบทนี้))

2)
เฉพาะในกรณีที่จำเป็นต้องใช้ผลข้างเคียงของพารามิเตอร์จริง แต่ไม่ต้องการค่าพารามิเตอร์เริ่มต้นโซลูชันที่มีประโยชน์คือdef ...(var1=None): if var1 is None: var1 = [] More

3) ในบางกรณีเป็นพฤติกรรมที่ไม่แน่นอนของค่าเริ่มต้นที่มีประโยชน์


5
ฉันหวังว่าคุณจะทราบว่า Python ไม่ใช่ภาษาโปรแกรมที่ใช้งานได้
Veky

6
ใช่ Python เป็นภาษาแบบหลาย paragigm พร้อมคุณสมบัติการทำงานบางอย่าง ("อย่าทำให้ทุกปัญหาดูเหมือนกับเล็บเพียงเพราะคุณมีค้อน") หลายคนอยู่ในแนวทางปฏิบัติที่ดีที่สุดของ Python Python มีฟังก์ชั่นการเขียนโปรแกรม HOWTO ที่น่าสนใจคุณสมบัติอื่น ๆ คือการปิดและการ currying ไม่ได้กล่าวถึงที่นี่
hynekcer

1
ฉันยังเพิ่มในช่วงปลายนี้ความหมายที่ได้รับมอบหมายของ Python ได้รับการออกแบบอย่างชัดเจนเพื่อหลีกเลี่ยงการคัดลอกข้อมูลในกรณีที่จำเป็นดังนั้นการสร้างสำเนา (และโดยเฉพาะอย่างยิ่งสำเนาลึก) จะส่งผลกระทบต่อการใช้งาน ดังนั้นจึงควรใช้เมื่อจำเป็นเท่านั้น แต่ผู้มาใหม่มักจะมีปัญหาในการทำความเข้าใจเมื่อเป็นเช่นนั้น
holdenweb

1
@holdenweb ฉันเห็นด้วย สำเนาชั่วคราวเป็นวิธีปกติที่สุดและบางครั้งเป็นวิธีเดียวที่เป็นไปได้ในการปกป้องข้อมูลที่ไม่สามารถเปลี่ยนแปลงได้จากฟังก์ชั่นภายนอกที่สามารถแก้ไขได้ โชคดีที่ฟังก์ชั่นที่แก้ไขข้อมูลอย่างไม่มีเหตุผลถือว่าเป็นจุดบกพร่องดังนั้นจึงถือเป็นเรื่องผิดปกติ
hynekcer

ฉันเห็นด้วยกับคำตอบนี้ และฉันไม่เข้าใจว่าทำไมการdef f( a = None )ก่อสร้างจึงแนะนำเมื่อคุณหมายถึงอย่างอื่น การคัดลอกก็โอเคเพราะคุณไม่ควรเปลี่ยนแปลงการโต้แย้ง และเมื่อคุณทำif a is None: a = [1, 2, 3]คุณจะคัดลอกรายการต่อไป
koddo

30

สิ่งนี้ไม่มีส่วนเกี่ยวข้องกับค่าเริ่มต้นนอกจากนี้มันมักจะเกิดขึ้นเป็นพฤติกรรมที่ไม่คาดคิดเมื่อคุณเขียนฟังก์ชันด้วยค่าเริ่มต้นที่ไม่แน่นอน

>>> 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 หรือไม่และมีข้อโต้แย้งเริ่มต้นที่เกี่ยวข้อง

หากคุณต้องการจัดการชั่วคราวในท้องถิ่นแบบทำลายล้างในระหว่างการคำนวณบางสิ่งและคุณต้องเริ่มการจัดการของคุณจากค่าอาร์กิวเมนต์คุณต้องทำสำเนา


7
แม้ว่าที่เกี่ยวข้องฉันคิดว่านี่เป็นพฤติกรรมที่แตกต่าง (ตามที่เราคาดหวังว่าappendจะเปลี่ยนa"แบบแทนที่") นั่นเป็นค่าเริ่มต้นที่ไม่แน่นอนในการโทรซ้ำแต่ละครั้งคือบิต "ไม่คาดคิด" ... อย่างน้อยสำหรับฉัน :)
Andy Hayden

2
@AndyHayden หากฟังก์ชั่นที่คาดว่าจะแก้ไขข้อโต้แย้งทำไมมันทำให้รู้สึกถึงมีค่าเริ่มต้น?
Mark Ransom

@MarkRansom cache={}ตัวอย่างเดียวที่ฉันจะคิดว่ามี อย่างไรก็ตามฉันสงสัยว่า "ความประหลาดใจน้อยที่สุด" นี้เกิดขึ้นเมื่อคุณไม่ได้คาดหวัง (หรือต้องการ) ฟังก์ชั่นที่คุณโทรมาเพื่อทำการโต้แย้ง
Andy Hayden

1
@ AndyHayden ฉันทิ้งคำตอบไว้ที่นี่ด้วยความเชื่อมั่นที่เพิ่มขึ้น แจ้งให้เราทราบสิ่งที่คุณคิด. ฉันอาจเพิ่มตัวอย่างของคุณcache={}ลงไปเพื่อความสมบูรณ์
Mark Ransom

1
@AndyHayden จุดคำตอบของฉันคือถ้าคุณประหลาดใจโดยบังเอิญกลายเป็นค่าเริ่มต้นของการโต้แย้งแล้วคุณมีข้อผิดพลาดอื่นซึ่งเป็นรหัสของคุณสามารถกลายพันธุ์โดยไม่ตั้งใจค่าโทรเมื่อเริ่มต้นไม่ได้ใช้ และโปรดทราบว่าการใช้Noneและการกำหนดค่าเริ่มต้นจริงถ้าหาเรื่องNone ไม่สามารถแก้ไขปัญหาได้ (ฉันคิดว่ามันเป็นรูปแบบการต่อต้านด้วยเหตุผลนั้น) หากคุณแก้ไขข้อผิดพลาดอื่น ๆ โดยหลีกเลี่ยงการกลายพันธุ์ค่าอาร์กิวเมนต์ว่ามีค่าเริ่มต้นหรือไม่ก็ตามคุณจะไม่สังเกตเห็นหรือสนใจเกี่ยวกับพฤติกรรม "ประหลาดใจ" นี้
Ben

27

หัวข้อยุ่งอยู่แล้ว แต่จากสิ่งที่ฉันอ่านที่นี่สิ่งต่อไปนี้ช่วยให้ฉันรู้ว่ามันทำงานอย่างไรภายใน:

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

2
จริงนี้อาจจะมีบิตสับสนสำหรับผู้มาใหม่เป็นa = a + [1]overloads a... พิจารณาเปลี่ยนไปและเพิ่มบรรทัดb = a + [1] ; print id(b) a.append(2)ที่จะทำให้มันชัดเจนมากขึ้นว่า+ในสองรายการจะสร้างรายการใหม่ (รับมอบหมายให้b) ในขณะที่การแก้ไขยังสามารถมีเดียวกันa id(a)
Jörn Hees

25

เป็นการเพิ่มประสิทธิภาพ จากฟังก์ชั่นนี้คุณคิดว่าการโทรทั้งสองฟังก์ชั่นใดเร็วกว่ากัน?

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 คุณมีค่าคงที่ที่ค่อนข้างอิสระ ใน Python คุณไม่มีสิทธิประโยชน์นี้


24

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

24

คำตอบที่สั้นที่สุดน่าจะเป็น "คำจำกัดความคือการประมวลผล" ดังนั้นอาร์กิวเมนต์ทั้งหมดจึงไม่สมเหตุสมผล เป็นตัวอย่างที่วางแผนไว้มากขึ้นคุณอาจอ้างถึงสิ่งนี้:

def a(): return []

def b(x=a()):
    print x

หวังว่าจะเพียงพอที่จะแสดงให้เห็นว่าการไม่ดำเนินการนิพจน์อาร์กิวเมนต์เริ่มต้น ณ เวลาดำเนินการของdefคำสั่งนั้นไม่ใช่เรื่องง่ายหรือไม่สมเหตุสมผลหรือทั้งสองอย่าง

ฉันยอมรับว่าเป็น gotcha เมื่อคุณพยายามใช้ตัวสร้างเริ่มต้น


20

วิธีแก้ปัญหาง่ายๆโดยใช้ None

>>> 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]

19

พฤติกรรมนี้ไม่น่าแปลกใจหากคุณพิจารณาสิ่งต่อไปนี้:

  1. พฤติกรรมของแอตทริบิวต์คลาสอ่านอย่างเดียวเมื่อพยายามกำหนดและ
  2. ฟังก์ชั่นเป็นวัตถุ (อธิบายได้ดีในคำตอบที่ยอมรับ)

บทบาทของ(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

19

ฉันจะแสดงให้เห็นถึงโครงสร้างทางเลือกเพื่อส่งผ่านรายการค่าเริ่มต้นไปยังฟังก์ชั่น (มันใช้ได้ดีกับพจนานุกรม)

ในขณะที่คนอื่น ๆ ได้แสดงความคิดเห็นอย่างกว้างขวางพารามิเตอร์รายการจะถูกผูกไว้กับฟังก์ชั่นเมื่อมันถูกกำหนดเมื่อเทียบกับเมื่อมันถูกดำเนินการ เนื่องจากรายการและพจนานุกรมไม่สามารถเปลี่ยนแปลงได้การเปลี่ยนแปลงใด ๆ ในพารามิเตอร์นี้จะส่งผลต่อการเรียกใช้ฟังก์ชันนี้อื่น ๆ ดังนั้นการเรียกฟังก์ชั่นที่ตามมาจะได้รับรายการที่แบ่งปันนี้ซึ่งอาจมีการเปลี่ยนแปลงโดยการโทรอื่น ๆ ไปยังฟังก์ชั่น ยิ่งไปกว่านั้นสองพารามิเตอร์กำลังใช้พารามิเตอร์ที่ใช้ร่วมกันของฟังก์ชั่นนี้ในเวลาเดียวกันโดยไม่คำนึงถึงการเปลี่ยนแปลงที่ทำโดยคนอื่น

วิธีการที่ไม่ถูกต้อง (อาจ ... ) :

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

Bython Slatkin ของ "Python ที่มีประสิทธิภาพ: 59 วิธีเฉพาะในการเขียน Python ที่ดีกว่า", รายการ 20: ใช้Noneและเอกสารประกอบเพื่อระบุอาร์กิวเมนต์เริ่มต้นแบบไดนามิก (หน้า 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]

อาจมีกรณีการใช้งานที่ถูกต้องสำหรับ 'วิธีการที่ไม่ถูกต้อง' โดยโปรแกรมเมอร์ตั้งใจให้พารามิเตอร์รายการเริ่มต้นถูกแชร์ แต่มีแนวโน้มว่าเป็นข้อยกเว้นมากกว่ากฎ


17

การแก้ปัญหาที่นี่คือ:

  1. ใช้Noneเป็นค่าเริ่มต้นของคุณ (หรือ nonce object) และเปิดเพื่อสร้างค่าของคุณในขณะทำงาน หรือ
  2. ใช้lambdaเป็นพารามิเตอร์เริ่มต้นของคุณและเรียกมันว่าภายในบล็อกลองเพื่อรับค่าเริ่มต้น (นี่คือประเภทของสิ่งที่แลมบ์ดาเป็นนามธรรม)

ตัวเลือกที่สองนั้นดีเพราะผู้ใช้ฟังก์ชั่นสามารถส่งผ่าน callable ซึ่งอาจมีอยู่แล้ว (เช่น a type)


16

เมื่อเราทำสิ่งนี้:

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]

16

บางครั้งฉันใช้ประโยชน์จากพฤติกรรมนี้แทนรูปแบบต่อไปนี้:

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()

ฉันใช้สิ่งนี้เพื่อสร้างอินสแตนซ์ของคลาสไคลเอนต์ที่เข้าถึงทรัพยากรภายนอกและสำหรับการสร้าง dicts หรือรายการสำหรับบันทึก

เนื่องจากฉันไม่คิดว่ารูปแบบนี้เป็นที่รู้จักกันดีฉันจะใส่ความคิดเห็นสั้น ๆ เพื่อป้องกันความเข้าใจผิดในอนาคต


2
ฉันชอบที่จะเพิ่มมัณฑนากรสำหรับการบันทึกและใส่แคชบันทึกช่วยจำลงบนวัตถุฟังก์ชั่นของตัวเอง
Stefano Borini

ตัวอย่างนี้ไม่ได้แทนที่รูปแบบที่ซับซ้อนยิ่งขึ้นที่คุณแสดงเนื่องจากคุณโทรหา_make_singletonเวลา def ในตัวอย่างอาร์กิวเมนต์เริ่มต้น แต่เวลาโทรในตัวอย่างทั่วโลก การทดแทนที่แท้จริงจะใช้กล่องที่ไม่แน่นอนบางประเภทสำหรับค่าอาร์กิวเมนต์เริ่มต้น แต่การเพิ่มอาร์กิวเมนต์ทำให้โอกาสผ่านค่าทางเลือกได้
Yann Vernier

15

คุณสามารถปัดเศษสิ่งนี้ด้วยการแทนที่วัตถุ (ดังนั้นการผูกด้วยขอบเขต):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

น่าเกลียด แต่ก็ใช้งานได้


3
นี่เป็นวิธีแก้ปัญหาที่ดีในกรณีที่คุณใช้ซอฟต์แวร์สร้างเอกสารอัตโนมัติเพื่อจัดทำเอกสารประเภทของข้อโต้แย้งที่คาดหวังจากฟังก์ชั่น การใส่ = ไม่มีแล้วตั้งค่าเป็น [] หาก a คือไม่มีช่วยให้ผู้อ่านเข้าใจได้อย่างรวดเร็วสิ่งที่คาดหวัง
Michael Scott Cuthbert

แนวคิดที่ยอดเยี่ยม: การปฏิเสธชื่อนั้นรับรองว่าจะไม่สามารถแก้ไขได้ ฉันชอบมันมาก
holdenweb

นี่เป็นวิธีที่จะทำ Python ไม่ได้ทำสำเนาของพารามิเตอร์ดังนั้นมันขึ้นอยู่กับคุณที่จะทำสำเนาอย่างชัดเจน เมื่อคุณมีสำเนาคุณสามารถแก้ไขได้ตามต้องการโดยไม่มีผลข้างเคียงที่ไม่คาดคิด
Mark Ransom

13

มันอาจเป็นจริงที่:

  1. มีคนใช้คุณสมบัติทุกภาษา / ไลบรารีและ
  2. การเปลี่ยนพฤติกรรมที่นี่จะไม่เหมาะสม แต่

มันสอดคล้องกันโดยสิ้นเชิงกับคุณสมบัติทั้งสองด้านบนและยังคงเป็นประเด็นต่อไป:

  1. มันเป็นคุณสมบัติที่ทำให้เกิดความสับสนและเป็นโชคร้ายใน Python

คำตอบอื่น ๆ หรืออย่างน้อยบางคนทำคะแนน 1 และ 2 แต่ไม่ใช่ 3 หรือทำคะแนน 3 และชี้ลง 1 และ 2 แต่ทั้งสามเป็นจริง

อาจเป็นเรื่องจริงที่การสลับม้าในกลางน้ำที่นี่จะขอให้มีการแตกหักที่สำคัญและอาจมีปัญหามากกว่านี้ที่เกิดจากการเปลี่ยน Python ให้จัดการสนิปเพตของ Stefano โดยสังเขป และมันอาจเป็นความจริงที่คนที่รู้จัก Python ภายในสามารถอธิบายผลที่เกิดขึ้นจริงได้ อย่างไรก็ตาม

พฤติกรรมที่มีอยู่ไม่ได้เป็น Pythonic และ Python ก็ประสบความสำเร็จเพราะมีน้อยมากเกี่ยวกับภาษาที่ละเมิดหลักการของความประหลาดใจอย่างน้อยทุกที่ที่อยู่ใกล้นี้ไม่ดี มันเป็นปัญหาที่แท้จริงไม่ว่าจะเป็นการถอนรากหรือไม่ก็ตาม มันเป็นข้อบกพร่องในการออกแบบ หากคุณเข้าใจภาษาที่ดีขึ้นโดยพยายามติดตามพฤติกรรมฉันสามารถพูดได้ว่า C ++ ทำสิ่งนี้และอื่น ๆ ทั้งหมด คุณเรียนรู้มากโดยการนำตัวอย่างเช่นข้อผิดพลาดตัวชี้ที่ละเอียดอ่อน แต่นี่ไม่ใช่ Pythonic: คนที่สนใจเกี่ยวกับ Python มากพอที่จะอดทนต่อการเผชิญกับพฤติกรรมนี้คือคนที่ถูกดึงดูดให้ใช้ภาษาเพราะ Python มีความประหลาดใจน้อยกว่าภาษาอื่น ๆ Dabblers และผู้อยากรู้อยากเห็นกลายเป็น Pythonistas เมื่อพวกเขาประหลาดใจในเวลาเพียงเล็กน้อยที่จะทำให้บางสิ่งบางอย่างทำงานได้ - ไม่ใช่เพราะการออกแบบ - ฉันหมายถึงปริศนาตรรกะที่ซ่อนอยู่ - ที่ตัดกับสัญชาตญาณของโปรแกรมเมอร์ เพราะมันใช้งานได้จริง


6
-1 ถึงมุมมองที่ป้องกันได้ แต่นี่ไม่ใช่คำตอบและฉันไม่เห็นด้วย ข้อยกเว้นพิเศษมากเกินไปทำให้เกิดมุมของตัวเอง
Marcin

3
ดังนั้นมันจึงเป็น "ไม่รู้งันน่าอัศจรรย์" ที่จะบอกว่าในงูหลามมันจะทำให้ความรู้สึกมากขึ้นสำหรับอาร์กิวเมนต์เริ่มต้นของ [] ยังคง [] ทุกครั้งที่ฟังก์ชั่นที่เรียกว่า?
Christos Hayward

3
และมันก็ไม่รู้ที่จะพิจารณาว่าเป็นสำนวนที่โชคร้ายที่ตั้งค่าอาร์กิวเมนต์เริ่มต้นเป็นไม่มีแล้วในร่างกายของร่างกายของการตั้งค่าฟังก์ชั่นถ้าอาร์กิวเมนต์ == ไม่มี: อาร์กิวเมนต์ = []? มันไม่รู้หรือไม่ที่จะพิจารณาสำนวนนี้ว่าโชคร้ายบ่อยครั้งที่ผู้คนต้องการสิ่งที่ผู้มาใหม่ไร้เดียงสาคาดหวังว่าถ้าคุณกำหนด f (อาร์กิวเมนต์ = []) การโต้แย้งจะเริ่มต้นโดยอัตโนมัติเป็นค่าของ []
Christos Hayward

3
แต่ในไพ ธ อนส่วนหนึ่งของจิตวิญญาณของภาษาคือคุณไม่จำเป็นต้องดำน้ำลึกมากเกินไป array.sort () ทำงานและทำงานโดยไม่คำนึงถึงว่าคุณเข้าใจเกี่ยวกับการเรียงลำดับ big-O และค่าคงที่น้อยเพียงใด ความงามของงูหลามในกลไกการเรียงลำดับอาเรย์เพื่อเป็นตัวอย่างที่นับไม่ถ้วนคือการที่คุณไม่จำเป็นต้องดำดิ่งลึกเข้าไปใน internals ความงามของงูหลามก็คือไม่จำเป็นต้องใช้วิธีปกติในการดำน้ำลึกเพื่อนำไปใช้เพื่อให้ได้สิ่งที่ Just Works และมีวิธีแก้ปัญหา (... ถ้าอาร์กิวเมนต์ == ไม่มี: อาร์กิวเมนต์ = []), FAIL
Christos Hayward

3
ในฐานะสแตนด์อโลนคำสั่งx=[]หมายถึง "สร้างวัตถุรายการว่างและผูกชื่อ 'x' ไว้กับวัตถุนั้น" ดังนั้นในdef f(x=[])รายการว่างจะถูกสร้างขึ้นด้วย มันไม่ได้ถูกผูกไว้กับ x เสมอไปดังนั้นแทนที่จะถูกผูกไว้กับตัวแทนตัวแทน ต่อมาเมื่อเรียกใช้ f () ค่าเริ่มต้นจะถูกดึงออกและผูกไว้กับ x เนื่องจากเป็นรายการว่างเปล่าที่ถูกกระรอกออกไปรายการเดียวกันจึงเป็นสิ่งเดียวที่ผูกกับ x ไม่ว่าจะมีสิ่งใดติดอยู่ข้างในหรือไม่ มันจะเป็นอย่างอื่นได้อย่างไร?
Jerry B

10

นี้ไม่ได้เป็นข้อบกพร่องในการออกแบบ ใครก็ตามที่เดินทางข้ามสิ่งนี้กำลังทำสิ่งผิดปกติ

มี 3 กรณีที่ฉันเห็นว่าคุณอาจพบปัญหานี้:

  1. คุณตั้งใจจะแก้ไขข้อโต้แย้งเป็นผลข้างเคียงของฟังก์ชั่น ในกรณีนี้มันไม่มีเหตุผลที่จะมีอาร์กิวเมนต์เริ่มต้น ข้อยกเว้นเพียงอย่างเดียวคือเมื่อคุณใช้รายการอาร์กิวเมนต์ในทางที่ผิดเพื่อให้มีฟังก์ชั่นฟังก์ชั่นเช่นcache={}และคุณจะไม่ถูกคาดหวังให้เรียกใช้ฟังก์ชันด้วยอาร์กิวเมนต์จริงเลย
  2. คุณตั้งใจจะปล่อยให้อาร์กิวเมนต์ไม่ได้แก้ไข แต่คุณได้ทำการแก้ไขโดยไม่ตั้งใจ นั่นเป็นข้อผิดพลาดแก้ไขได้
  3. คุณตั้งใจจะแก้ไขอาร์กิวเมนต์เพื่อใช้ภายในฟังก์ชั่น แต่ไม่ได้คาดหวังว่าการแก้ไขจะสามารถดูได้นอกฟังก์ชั่น ในกรณีที่คุณต้องทำสำเนาของการโต้แย้งไม่ว่าจะเป็นค่าเริ่มต้นหรือไม่! Python ไม่ใช่ภาษาการโทรตามค่าดังนั้นจึงไม่ได้คัดลอกสำหรับคุณคุณต้องมีความชัดเจนเกี่ยวกับเรื่องนี้

ตัวอย่างในคำถามอาจอยู่ในหมวดที่ 1 หรือ 3 มันแปลกที่ทั้งคู่แก้ไขรายการที่ส่งผ่านแล้วส่งคืน คุณควรเลือกอย่างใดอย่างหนึ่ง


"ทำสิ่งผิดปกติ" คือการวินิจฉัย ที่กล่าวว่าฉันคิดว่ามีหลายครั้งที่ = ไม่มีรูปแบบมีประโยชน์ แต่โดยทั่วไปคุณไม่ต้องการแก้ไขหากผ่านการเปลี่ยนแปลงในกรณีนั้น (2) cache={}รูปแบบที่เป็นจริงการแก้ปัญหาให้สัมภาษณ์อย่างเดียวในรหัสจริงคุณอาจต้องการ@lru_cache!
Andy Hayden

9

"บั๊ก" นี้ให้เวลาทำงานล่วงเวลาเยอะมาก! แต่ฉันเริ่มเห็นความเป็นไปได้ที่จะใช้มัน (แต่ฉันก็อยากให้มันเป็นช่วงเวลาการประหารชีวิต)

ฉันจะให้สิ่งที่ฉันเห็นเป็นตัวอย่างที่มีประโยชน์

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

8

เพียงเปลี่ยนฟังก์ชั่นเป็น:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

ฉันคิดว่าคำตอบสำหรับคำถามนี้อยู่ในวิธีที่ python ส่งผ่านข้อมูลไปยังพารามิเตอร์ (ผ่านค่าหรืออ้างอิง) ไม่ใช่ความแปรปรวนหรือวิธีที่ python จัดการกับคำสั่ง "def"

แนะนำสั้น ๆ เริ่มแรกมีชนิดข้อมูลสองชนิดในหลามหนึ่งคือชนิดข้อมูลพื้นฐานอย่างง่ายเช่นตัวเลขและชนิดข้อมูลอื่นคือวัตถุ ประการที่สองเมื่อส่งข้อมูลไปยังพารามิเตอร์ 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(อดีตไม่แน่นอนในขณะที่หลังไม่เปลี่ยนรูป แต่ความผันแปรไม่ได้เกี่ยวข้องกับคำถาม) ไม่มีใครอยู่ในพื้นที่ แต่เรารู้ว่ามันอยู่ที่นั่นและมีสำเนาหนึ่งฉบับไม่มี ดังนั้นทุกครั้งที่มีการเรียก foo ไอเท็มจะถูกประเมิน (ตรงข้ามกับคำตอบที่ได้รับการประเมินเพียงครั้งเดียว) เป็นไม่มีเพื่อให้ชัดเจนการอ้างอิง (หรือที่อยู่) ของไม่มี จากนั้นในรายการ 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.Button นั้นถูกต้อง:

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คุ้มค่าของผู้อื่น


5
จุดสุดท้ายของคุณผิด ลองมันและคุณจะเห็นว่าเป็นx[7]() 9
Duncan

2
"python ส่งข้อมูลพื้นฐานโดยพิมพ์ตามค่ากล่าวคือการทำสำเนาโลคัลของค่าไปยังตัวแปรโลคัล" ไม่ถูกต้องสมบูรณ์ ฉันประหลาดใจที่บางคนสามารถรู้ Python ได้ดี แต่มีความเข้าใจผิดที่น่ากลัวของปัจจัยพื้นฐาน :-(
Veky

6

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ประเมินผล ณ เวลาดำเนินการจะไม่ส่งผลต่อเวลากำหนด การเลือก "at definition" อนุญาตให้แสดงทั้งสองกรณีขณะที่การเลือก "at execution" สามารถแสดงได้เพียงหนึ่งกรณี:

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
    ...

"ความสอดคล้องกล่าวแล้ว" ที่คำจำกัดความ ": ทุกสิ่งอื่น ๆ ของการdef <name>(<args=defaults>):ประเมินที่ดีที่สุดที่คำจำกัดความเช่นกัน" ฉันไม่คิดว่าข้อสรุปจะตามมาจากสถานที่ตั้ง เพียงเพราะสองสิ่งอยู่ในบรรทัดเดียวกันไม่ได้หมายความว่าควรประเมินในขอบเขตเดียวกัน defaultเป็นสิ่งที่แตกต่างจากส่วนที่เหลือของบรรทัด: มันคือการแสดงออก การประเมินการแสดงออกเป็นกระบวนการที่แตกต่างกันมากจากการกำหนดฟังก์ชั่น
LarsH

นิยามฟังก์ชัน @LarsH ถูกประเมินใน Python ไม่ว่าจะมาจากคำสั่ง ( def) หรือการแสดงออก ( lambda) ไม่เปลี่ยนแปลงว่าการสร้างฟังก์ชั่นหมายถึงการประเมินผล - โดยเฉพาะลายเซ็นของมัน และค่าเริ่มต้นเป็นส่วนหนึ่งของลายเซ็นของฟังก์ชั่น นั่นไม่ได้หมายความว่าจะต้องมีการประเมินค่าเริ่มต้นทันที - ตัวอย่างคำแนะนำประเภทอาจไม่ แต่มันแสดงให้เห็นอย่างแน่นอนว่าพวกเขาควรเว้นแต่จะมีเหตุผลที่ดีที่จะไม่
MisterMiyagi

ตกลงการสร้างฟังก์ชั่นหมายถึงการประเมินผลในบางแง่มุม แต่เห็นได้ชัดว่าไม่ได้ในแง่ที่ว่าทุกการแสดงออกภายในนั้นจะถูกประเมินในเวลาที่กำหนด ส่วนใหญ่ไม่ได้ มันไม่ชัดเจนสำหรับฉันในแง่ที่ว่าลายเซ็นโดยเฉพาะ "ประเมิน" ในเวลาที่กำหนดใด ๆ ที่เกินกว่าหน้าที่ของร่างกายคือ "ประเมิน" (แยกวิเคราะห์เป็นตัวแทนที่เหมาะสม); ในขณะที่การแสดงออกในการทำงานของร่างกายนั้นไม่ได้รับการประเมินอย่างชัดเจน จากมุมมองนี้ความคงเส้นคงวาจะบอกว่านิพจน์ในลายเซ็นไม่ควร "สมบูรณ์" อย่างเต็มที่
LarsH

ฉันไม่ได้หมายความว่าคุณผิดเพียง แต่ข้อสรุปของคุณไม่ได้ติดตามจากความมั่นคงเพียงอย่างเดียว
LarsH

@LarsH ค่าเริ่มต้นไม่ได้เป็นส่วนหนึ่งของร่างกายและฉันไม่ได้อ้างว่าความสอดคล้องเป็นเกณฑ์เท่านั้น คุณสามารถให้คำแนะนำวิธีการชี้แจงคำตอบได้หรือไม่?
MisterMiyagi

3

ทุกคำตอบอื่น ๆ อธิบายว่าทำไมนี่จึงเป็นพฤติกรรมที่ดีและต้องการหรือทำไมคุณไม่ควรต้องการสิ่งนี้ ของฉันมีไว้สำหรับคนที่ดื้อรั้นที่ต้องการใช้สิทธิ์ในการงอภาษาตามความต้องการของพวกเขาไม่ใช่วิธีอื่น

เราจะ "แก้ไข" พฤติกรรมนี้ด้วยมัณฑนากรที่จะคัดลอกค่าเริ่มต้นแทนที่จะนำอินสแตนซ์เดียวกันกลับมาใช้ใหม่สำหรับแต่ละอาร์กิวเมนต์ตำแหน่งที่เหลืออยู่ที่ค่าเริ่มต้น

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])

มัณฑนากรสามารถปรับได้เพื่อให้ได้ แต่เราปล่อยให้สิ่งนี้เป็นการออกกำลังกายสำหรับผู้อ่าน;)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.