ลอง / ยกเว้นบล็อกในหลามเป็นแบบฝึกหัดการเขียนโปรแกรมที่ดีหรือไม่


201

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

dict_container = DictContainer()
dict_container['foo'] = bar
...
print dict_container.foo

ฉันรู้ว่ามันอาจงี่เง่าที่จะเขียนบางอย่างเช่นนี้ แต่นั่นเป็นฟังก์ชั่นที่ฉันต้องการ ฉันคิดว่าจะใช้สิ่งนี้ด้วยวิธีต่อไปนี้:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        try:
            return self.dict[item]
        except KeyError:
            print "The object doesn't have such attribute"

ฉันไม่แน่ใจว่าการลอง / ยกเว้นบล็อกเป็นวิธีปฏิบัติที่ดีหรือไม่ดังนั้นจึงควรใช้อีกวิธีหนึ่งhasattr()และhas_key():

def __getattribute__(self, item):
        if hasattr(self, item):
            return object.__getattribute__(item)
        else:
            if self.dict.has_key(item):
                return self.dict[item]
            else:
                raise AttributeError("some customised error")

หรือใช้หนึ่งในนั้นและลองใช้ catch block ดังนี้:

def __getattribute__(self, item):
    if hasattr(self, item):
        return object.__getattribute__(item)
    else:
        try:
            return self.dict[item]
        except KeyError:
            raise AttributeError("some customised error")

ตัวเลือกแบบไหนที่ไพเราะและสง่างามที่สุด?


if 'foo' in dict_container:จะขอบคุณพระเจ้าหลามให้ สาธุ
gseattle

คำตอบ:


181

ตัวอย่างแรกของคุณดีมาก แม้ทางการเอกสารหลามแนะนำรูปแบบนี้เป็นที่รู้จักกันเป็นEAFP

โดยส่วนตัวแล้วฉันชอบหลีกเลี่ยงการทำรังเมื่อไม่จำเป็น:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass  # fallback to dict
    try:
        return self.dict[item]
    except KeyError:
        raise AttributeError("The object doesn't have such attribute") from None

PS has_key()เลิกใช้มานานแล้วใน Python 2 ใช้item in self.dictแทน


2
return object.__getattribute__(item)ไม่ถูกต้องและจะสร้าง a TypeErrorเนื่องจากมีการส่งจำนวนอาร์กิวเมนต์ที่ไม่ถูกต้อง return object.__getattribute__(self, item)มันควรจะเป็น
martineau

13
PEP 20: แบนดีกว่าซ้อนกัน
Ioannis Filippidis

7
อะไรfrom Noneหมายถึงในบรรทัดสุดท้าย?
niklas

2
@niklas เป็นสิ่งสำคัญที่จะระงับบริบทข้อยกเว้น ("ในระหว่างการจัดการข้อยกเว้นนี้มีข้อยกเว้นอื่นเกิดขึ้น" - ข้อความย่อย) ดูที่นี่
Kade

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

19

ในขณะที่ใน Java แน่นอนการปฏิบัติที่ไม่ดีที่จะใช้ข้อยกเว้นสำหรับการควบคุมการไหล (ส่วนใหญ่เป็นเพราะข้อยกเว้นบังคับ JVM ในการรวบรวมทรัพยากร ( เพิ่มเติมได้ที่นี่ )) ในหลามคุณมี 2 หลักการสำคัญ: พิมพ์ดีดเป็ดและEAFP สิ่งนี้หมายความว่าคุณได้รับการกระตุ้นให้ลองใช้วัตถุในแบบที่คุณคิดว่ามันใช้งานได้และจัดการเมื่อสิ่งต่าง ๆ ไม่เป็นเช่นนั้น

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


10

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

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass
    # execution only reaches here when try block raised AttributeError
    try:
        return self.dict[item]
    except KeyError:
        print "The object doesn't have such attribute"

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

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


10

ระวังตัวด้วย - ในกรณีfinallyนี้สัมผัสก่อน แต่ข้ามไปเช่นกัน

def a(z):
    try:
        100/z
    except ZeroDivisionError:
        try:
            print('x')
        finally:
            return 42
    finally:
        return 1


In [1]: a(0)
x
Out[1]: 1

ว้าวมันทำให้ฉันคิดถึง ... คุณช่วยชี้ให้ฉันดูบางส่วนของเอกสารที่อธิบายพฤติกรรมนี้ได้หรือไม่?
Michal

1
@Michal: fyi: finallyบล็อกทั้งสองจะถูกดำเนินการa(0)แต่finally-returnจะส่งกลับเฉพาะผู้ปกครอง
Sławomir Lenart

7

ในความคิดของฉันนี้จะเป็นวิธีที่ Pythonic ที่สุดในการจัดการแม้ว่าและเพราะมันทำให้คำถามของคุณสงสัย โปรดทราบว่าสิ่งนี้กำหนด__getattr__()แทน__getattribute__()เพราะการทำเช่นนั้นหมายความว่าจะต้องจัดการกับคุณลักษณะ "พิเศษ" ที่ถูกเก็บไว้ในพจนานุกรมภายในเท่านั้น

def __getattr__(self, name):
    """only called when an attribute lookup in the usual places has failed"""
    try:
        return self.my_dict[name]
    except KeyError:
        raise AttributeError("some customized error message")

2
โปรดทราบว่าการเพิ่มข้อยกเว้นในexceptบล็อกอาจให้ผลลัพธ์ที่สับสนใน Python 3 นั่นเป็นเพราะ (ต่อ PEP 3134) Python 3 ติดตามข้อยกเว้นแรก (the KeyError) เป็น "บริบท" ของข้อยกเว้นที่สอง (the AttributeError) และหากถึง ระดับบนสุดก็จะพิมพ์ร่องรอยที่มีข้อยกเว้นทั้งสอง สิ่งนี้จะมีประโยชน์เมื่อไม่คาดว่าจะมีข้อยกเว้นที่สอง แต่ถ้าคุณตั้งใจเพิ่มข้อยกเว้นที่สองมันไม่เป็นที่พึงปรารถนา สำหรับงูหลาม 3.3, PEP 415 raise AttributeError("whatever") from Noneเพิ่มความสามารถในการปราบปรามบริบทโดยใช้
Blckknght

3
@Blckknght: การพิมพ์การสืบค้นกลับที่มีข้อยกเว้นทั้งสองอย่างจะใช้ได้ในกรณีนี้ กล่าวอีกนัยหนึ่งฉันไม่คิดว่าคำแถลงแบบผ้าห่มของคุณว่ามันไม่เป็นที่พึงปรารถนาเสมอไป ในการใช้งานที่นี่ก็เปลี่ยนKeyErrorเป็นAttributeErrorและแสดงให้เห็นว่าสิ่งที่เกิดขึ้นใน traceback จะเป็นประโยชน์และเหมาะสม
martineau

สำหรับสถานการณ์ที่ซับซ้อนมากขึ้นคุณอาจพูดถูก แต่ฉันคิดว่าเมื่อคุณแปลงระหว่างประเภทยกเว้นคุณมักจะรู้ว่ารายละเอียดของข้อยกเว้นแรกไม่สำคัญสำหรับผู้ใช้ภายนอก นั่นคือถ้า__getattr__ยกข้อผิดพลาดข้อผิดพลาดน่าจะเป็นข้อผิดพลาดในการเข้าถึงคุณลักษณะที่ไม่ได้เป็นข้อผิดพลาดการใช้งานในรหัสของคลาสปัจจุบัน การแสดงข้อยกเว้นก่อนหน้านี้เนื่องจากบริบทสามารถทำให้ยุ่งเหยิงได้ และแม้กระทั่งเมื่อคุณปราบปรามบริบทที่มีคุณสามารถยังคงได้รับข้อยกเว้นที่ก่อนหน้านี้ในกรณีที่จำเป็นผ่านทางraise Whatever from None ex.__context__
Blckknght

1
ฉันต้องการยอมรับคำตอบของคุณ แต่ในคำถามที่ฉันอยากรู้เพิ่มเติมว่าการใช้ try / catch แบบซ้อนเป็นวิธีปฏิบัติที่ดีหรือไม่ ในทางกลับกันมันเป็นคำตอบที่ดีที่สุดและฉันจะใช้มันในโค้ดของฉัน ขอบคุณมากมาร์ติน
Michal

Michal: ไม่เป็นไร __getattribute__()นอกจากนี้ยังเร็วกว่าการใช้
martineau

4

ใน Python ง่ายต่อการขอการอภัยมากกว่าการอนุญาต อย่าเหงื่อจัดการข้อยกเว้นซ้อน

(นอกจากนี้has*มักใช้ข้อยกเว้นภายใต้ฝาครอบอยู่เสมอ)


4

ตามเอกสารมันจะดีกว่าที่จะจัดการข้อยกเว้นหลายอย่างผ่าน tuples หรือเช่นนี้

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise

2
คำตอบนี้ไม่ได้ตอบคำถามดั้งเดิม แต่สำหรับทุกคนที่อ่านมันให้สังเกตว่า "เปลือย" ยกเว้นท้ายที่สุดเป็นความคิดที่แย่มาก (โดยปกติ) เพราะมันจะจับทุกอย่างรวมถึงเช่น NameError & KeyboardInterrupt - ซึ่งไม่ใช่สิ่งที่คุณหมายถึง!
แพ้

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

4

ตัวอย่างที่ดีและเรียบง่ายสำหรับลอง / ยกเว้นแบบซ้อนอาจเป็นดังต่อไปนี้:

import numpy as np

def divide(x, y):
    try:
        out = x/y
    except:
        try:
            out = np.inf * x / abs(x)
        except:
            out = np.nan
    finally:
        return out

ตอนนี้ลองชุดค่าผสมต่างๆแล้วคุณจะได้ผลลัพธ์ที่ถูกต้อง:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[แน่นอนเรามีจำนวนมากดังนั้นเราจึงไม่จำเป็นต้องสร้างฟังก์ชั่นนี้]


2

สิ่งหนึ่งที่ฉันต้องการหลีกเลี่ยงคือการเพิ่มข้อยกเว้นใหม่ในขณะที่จัดการกับสิ่งเก่า ทำให้ข้อความแสดงข้อผิดพลาดทำให้อ่านสับสน

ตัวอย่างเช่นในรหัสของฉันฉันเขียนตอนแรก

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    raise KeyError(key)

และฉันได้รับข้อความนี้

>>> During handling of above exception, another exception occurred.

สิ่งที่ฉันต้องการคือ:

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    pass
raise KeyError(key)

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


ดูการใช้คำตอบที่ได้รับการยอมรับfrom Noneสำหรับการเพิ่มคะแนนสไตล์ :)
เปียโนซอรัส

1

หากการลองแบบยกเว้นข้อสุดท้ายถูกบล็อกไว้ภายในบล็อกสุดท้ายผลลัพธ์จาก "child" จะถูกเก็บรักษาไว้ในที่สุด ฉันยังไม่พบการหมดอายุอย่างเป็นทางการ แต่ข้อมูลโค้ดต่อไปนี้แสดงพฤติกรรมนี้ใน Python 3.6

def f2():
    try:
        a = 4
        raise SyntaxError
    except SyntaxError as se:
        print('log SE')
        raise se from None
    finally:
        try:
            raise ValueError
        except ValueError as ve:
            a = 5
            print('log VE')
            raise ve from None
        finally:
            return 6       
        return a

In [1]: f2()
log SE
log VE
Out[2]: 6

พฤติกรรมนี้แตกต่างจากตัวอย่างที่กำหนดโดย @ Sławomir Lenart เมื่อสุดท้ายถูกซ้อนอยู่ภายในยกเว้นบล็อก
Guanghua Shu

0

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


จากเอกสาร: ในสภาพแวดล้อมแบบมัลติเธรดวิธี LBYL (Look Before You Leap) สามารถเสี่ยงต่อการแนะนำสภาพการแข่งขันระหว่าง“ การมอง” และ“ การกระโดด” ตัวอย่างเช่นรหัสหากคีย์ในการแมป: ส่งคืนการแมป [คีย์] อาจล้มเหลวหากเธรดอื่นเอาคีย์ออกจากการทำแผนที่หลังการทดสอบ แต่ก่อนการค้นหา ปัญหานี้สามารถแก้ไขได้ด้วยการล็อคหรือโดยการใช้ EAFP (ง่ายต่อการขอขมามากกว่าได้รับอนุญาต) วิธีการ
Nuno André
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.