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

Jul 16 2009

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

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

คำตอบ

1658 rob Jul 18 2009 at 04:29

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

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

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

278 EliCourtwright Jul 16 2009 at 01:11

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

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

244 glglgl Jul 10 2012 at 21:50

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

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

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
121 Utaal Jul 16 2009 at 06:21

ฉันไม่รู้อะไรเลยเกี่ยวกับการทำงานภายในของล่าม 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):
83 LennartRegebro Jul 16 2009 at 01:54

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

69 DimitrisFasarakisHilliard Dec 09 2015 at 14:13

ไม่รู้จะวิปัสสนาทำไม

ฉันแปลกใจจริงๆที่ไม่มีใครทำการวิปัสสนาอย่างลึกซึ้งที่ 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จะถูกสร้างขึ้น

59 Brian Jul 16 2009 at 17:05

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

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

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

55 LutzPrechelt Mar 30 2015 at 18:18

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

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

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

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

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

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

53 ymv Jul 16 2009 at 02:15

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

  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 การเปลี่ยนแปลง - ดำเนินการกับวัตถุเดียวกัน - และพิมพ์ออกมา
35 GlennMaynard Jul 16 2009 at 03:18

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

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

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

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

35 hynekcer Nov 23 2012 at 01:09

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) ในบางกรณีเป็นพฤติกรรมที่ไม่แน่นอนของค่าเริ่มต้นที่มีประโยชน์

31 Ben May 23 2011 at 11:24

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

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

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

27 Stéphane Mar 27 2015 at 06:14

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

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
25 JasonBaker Jul 16 2009 at 06:18

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

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 คุณไม่มีประโยชน์นี้

25 AaronHall May 01 2016 at 23:20

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 Baczek Jul 16 2009 at 19:19

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

def a(): return []

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

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

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

21 DmitryMinkovsky Apr 25 2012 at 02:43

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

  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]ซึ่งเป็นส่วนหนึ่งของวัตถุและยังคงอยู่ระหว่างการดำเนินการของรหัสในfoofoo

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

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
20 hugo24 Feb 28 2013 at 18:10

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

>>> 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 Alexander Sep 12 2015 at 13:00

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

ตามที่คนอื่น ๆ ได้แสดงความคิดเห็นไว้อย่างกว้างขวางพารามิเตอร์ 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]

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

17 Marcin Mar 21 2012 at 00:22

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

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

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

16 joedborg Jan 15 2013 at 18:02

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

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

น่าเกลียด แต่ได้ผล

16 Saish Sep 12 2014 at 05:05

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

def foo(a=[]):
    ...

... เรากำหนดอาร์กิวเมนต์aให้กับรายการที่ไม่มีชื่อหากผู้โทรไม่ส่งค่าของ.

เพื่อให้การสนทนานี้ง่ายขึ้นเรามาตั้งชื่อรายการที่ไม่มีชื่อชั่วคราว แล้วไงpavlo?

def foo(a=pavlo):
   ...

ณ เวลาใด ๆ ถ้าโทรไม่ได้บอกเราว่าสิ่งที่เป็นเรานำมาใช้ใหม่apavlo

ถ้าpavloเป็นไม่แน่นอน (แก้ไขได้) และfooจบลงด้วยการปรับเปลี่ยนมันมีผลเราแจ้งให้ทราบในครั้งต่อไปจะเรียกว่าไม่ต้องระบุfooa

นี่คือสิ่งที่คุณเห็น (โปรดจำไว้ว่า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 bgreen-litl Feb 06 2015 at 04:44

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

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

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

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

13 ChristosHayward Jul 17 2009 at 02:17

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

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

มันสอดคล้องกันอย่างสิ้นเชิงที่จะยึดคุณสมบัติทั้งสองข้างต้นและยังคงให้ประเด็นอื่น:

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

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

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

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

10 MarkRansom Oct 18 2017 at 00:38

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

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

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

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

9 Norfeldt Jul 22 2013 at 14:35

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

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

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 ytpillai May 26 2015 at 06:04

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

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
7 user2384994 Aug 22 2013 at 12:58

ฉันคิดว่าคำตอบสำหรับคำถามนี้อยู่ที่วิธีที่ 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.

6 MisterMiyagi Dec 15 2018 at 19:09

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
    ...
4 PrzemekD Jan 03 2019 at 14:38

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

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

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

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