Akış kontrolü istisnaları Python'da en iyi uygulama mıdır?


15

"Öğrenme Python" okuyorum ve aşağıdakilerle karşılaştım:

Kullanıcı tanımlı istisnalar, hata olmayan durumlara da işaret edebilir. Örneğin, bir eşleşme bulunduğunda arayanın yorumlaması için bir durum bayrağı döndürmek yerine bir istisna oluşturacak şekilde bir arama rutini kodlanabilir. Aşağıda, try / haric / else istisna işleyicisi bir if / else dönüş değeri test cihazının çalışmasını yapar:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Python dinamik olarak yazıldığından ve çekirdeğe polimorfik olduğundan, sentinel dönüş değerleri yerine istisnalar, bu tür durumları belirtmek için genellikle tercih edilen yoldur.

Çeşitli forumlarda defalarca tartışılan bu tür şeyleri gördüm ve döngüleri durdurmak için StopIteration kullanarak Python'a başvurular gördüm, ancak resmi stil kılavuzlarında fazla bir şey bulamıyorum (PEP 8, akış kontrolü istisnaları için bir hazırlıksız referansa sahiptir) veya geliştiricilerin ifadeleri. Bunun Python için en iyi uygulama olduğunu belirten resmi bir şey var mı?

Bu ( ? Ciddi antipattern kabul denetim akışı olarak istisnalar Are Eğer öyleyse, neden? ) De bu tarz Pythonic olduğunu birkaç yorum yapanlar durumu vardır. Bu neye dayanıyor?

TIA

Yanıtlar:


26

Genel fikir birliği “istisna kullanmayın!” çoğunlukla diğer dillerden geliyor ve hatta bazen modası geçmiş.

  • C ++ 'da, “yığın gevşemesi” nedeniyle istisna atmak çok masraflıdır . Her yerel değişken bildirimi withPython'daki bir ifadeye benzer ve bu değişken içindeki nesne yıkıcıları çalıştırabilir. Bu yıkıcılar bir istisna atıldığında değil, aynı zamanda bir işlevden döndüğünde yürütülür. Bu “RAII deyimi” ayrılmaz bir dil özelliğidir ve sağlam, doğru kod yazmak için çok önemlidir - bu nedenle RAII'nin ucuz istisnalara karşı C ++, RAII'ye karar verdiği bir ödünleşmedir.

  • Erken C ++ 'da, çok sayıda kod istisna açısından güvenli bir şekilde yazılmamıştır: aslında RAII kullanmıyorsanız, bellek ve diğer kaynaklara sızmak kolaydır. Dolayısıyla istisnalar atmak bu kodu yanlış yapar. C ++ standart kitaplığı bile istisnalar kullandığından artık mantıklı değil: İstisnalar yokmuş gibi davranamazsınız. Ancak, C kodu C ++ ile birleştirilirken istisnalar hala bir sorundur.

  • Java'da, her istisnanın ilişkili bir yığın izlemesi vardır. Hata izleme hata ayıklama sırasında yığın izlemesi çok değerlidir, ancak kural dışı durum hiçbir zaman yazdırılmadığında, örneğin yalnızca kontrol akışı için kullanıldığı için harcanan çabadır.

Dolayısıyla bu dillerde istisnalar kontrol akışı olarak kullanılmak için “çok pahalı” dır. Python'da bu daha az sorun ve istisnalar çok daha ucuz. Buna ek olarak, Python dili, diğer kontrol akışı yapılarına kıyasla istisnaların maliyetini farkedilmez hale getiren bir ek yükten muzdariptir: örneğin, açık bir üyelik testi ile bir dikte girişinin olup olmadığını kontrol etmek if key in the_dict: ...genellikle girişe erişmek kadar hızlıdır the_dict[key]; ...ve bir KeyError alın. Bazı ayrılmaz dil özellikleri (örneğin, jeneratörler) istisnalar açısından tasarlanmıştır.

Bu nedenle, Python'daki istisnalardan özellikle kaçınmak için teknik bir neden olmasa da, dönüş değerleri yerine bunları kullanmanız gerekip gerekmediği sorusu hala var. İstisnalar dışında tasarım düzeyinde sorunlar:

  • hiç belli değiller. Bir işleve kolayca bakamaz ve hangi istisnaları atabileceğini göremezsiniz, böylece ne yakalayacağınızı her zaman bilemezsiniz. Dönüş değeri daha iyi tanımlanma eğilimindedir.

  • istisnalar, kodunuzu karmaşıklaştıran yerel olmayan kontrol akışıdır. Bir istisna attığınızda, kontrol akışının nereye devam edeceğini bilmiyorsunuz. Hemen ele alınamayan hatalar için, bu muhtemelen iyi bir fikirdir, arayan kişiye bir durumu bildirirken bu tamamen gereksizdir.

Python kültürü genellikle istisnalar lehine eğimlidir, ancak denize girmek kolaydır. list_contains(the_list, item)Listenin o öğeye eşit bir öğe içerip içermediğini kontrol eden bir işlev düşünün . Sonuç kesinlikle sinir bozucu istisnalar aracılığıyla iletilirse, çünkü bunu şöyle çağırmalıyız:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Bir boole geri dönmek çok daha açık olurdu:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Fonksiyonun zaten bir değer döndürmesi gerekiyorsa, özel koşullar için özel bir değer döndürmek oldukça hataya açıktır, çünkü insanlar bu değeri kontrol etmeyi unutacaktır (muhtemelen C'deki sorunların 1 / 3'ünün nedeni budur). Bir istisna genellikle daha doğrudur.

Buna iyi bir örnek, samanlık dizesindeki dizenin pos = find_string(haystack, needle)ilk oluşumunu needlearaştıran ve başlangıç ​​konumunu döndüren bir işlevdir . Peki samanlık ipi iğne ipi içermiyorsa ne olur?

C'nin çözümü ve Python tarafından taklit edilmesi özel bir değer döndürmektir. C'de bu bir boş gösterici, Python'da bu -1. Konum, özellikle -1Python'daki geçerli bir dizin gibi , denetim olmadan dizgi dizini olarak kullanıldığında şaşırtıcı sonuçlar doğuracaktır . C olarak, NULL işaretçiniz size en azından bir segfault verecektir.

PHP'de, farklı türde özel bir değer döndürülür: FALSEtamsayı yerine boole . Anlaşıldığı gibi, bu, dilin örtülü dönüştürme kuralları nedeniyle daha iyi değildir (ancak Python'da booleanların ints olarak kullanılabileceğini unutmayın!). Tutarlı bir tür döndürmeyen işlevler genellikle çok kafa karıştırıcı kabul edilir.

Daha sağlam bir varyant, dize bulunamadığında bir istisna atmak olurdu, bu da normal kontrol akışı sırasında özel bir değeri yanlışlıkla sıradan bir değer yerine kullanmanın imkansız olmasını sağlar:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Alternatif olarak, her zaman doğrudan kullanılamayan ancak ilk olarak ambalajın açılması gereken bir türün döndürülmesi kullanılabilir, örneğin booleanın bir istisnanın meydana gelip gelmediğini veya sonucun kullanılabilir olup olmadığını gösteren bir sonuç-bool demet. Sonra:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Bu sizi derhal problemlerle başa çıkmaya zorlar, ancak çok çabuk sinir bozucu olur. Ayrıca kolayca zincirleme fonksiyonunu engeller. Artık her işlev çağrısı için üç satır kod gerekir. Golang, bu rahatsızlığın güvenliğe değer olduğunu düşünen bir dildir.

Özetlemek gerekirse, istisnalar tamamen problemsiz değildir ve özellikle “normal” bir dönüş değerinin yerini aldıklarında kesinlikle aşırı kullanılabilir. Ancak, özel koşullara işaret etmek için kullanıldığında (sadece hatalar değil), istisnalar temiz, sezgisel, kullanımı kolay ve kötüye kullanımı zor API'ler geliştirmenize yardımcı olabilir.


1
Ayrıntılı cevap için teşekkürler. "İstisnalar sadece istisnai durumlar için" Java gibi diğer dillerin arka planından geliyorum, bu yüzden bu benim için yeni. EAFP, EIBTI, vb. Gibi diğer Python yönergeleriyle (posta listelerinde, blog gönderilerinde vb.) Bu şekilde belirtmiş olan herhangi bir Python çekirdek geliştiricisi var mı? başka bir geliştirici / patron soruyor. Teşekkürler!
JB

Özel istisna sınıfınızın fillInStackTrace yöntemini hemen geri döndürmek için aşırı yükleyerek, pahalı olan yığın yürüyüşü olduğu ve bu nerede yapıldığı için yükseltmeyi çok ucuz hale getirebilirsiniz.
John Cowan

Girişinizdeki ilk merminiz ilk başta kafa karıştırıcıydı. Belki de "Modern C ++ 'da istisna işleme kodu sıfır maliyetlidir, ancak istisna atmak pahalıdır" gibi bir şeyle değiştirebilirsiniz. (
lurkers

A collections.defaultdictveya sözlük kullanarak sözlük arama işlemleri my_dict.get(key, default)için kodu çok daha açık hale getirirtry: my_dict[key] except: return default
Steve Barnes

11

HAYIR! - genel olarak değil - istisnalar, tek bir kod sınıfı hariç, iyi akış kontrolü uygulaması olarak kabul edilmez. İstisnaların bir koşulu işaret etmenin makul, hatta daha iyi bir yolu olduğu düşünülen yer jeneratör veya yineleyici işlemleridir. Bu işlemler olası herhangi bir değeri geçerli bir sonuç olarak döndürebilir, böylece bir sonlandırmaya işaret etmek için bir mekanizma gerekir.

Her seferinde bir baytlık ikili akış dosyasını okumayı düşünün - kesinlikle herhangi bir değer potansiyel olarak geçerli bir sonuçtur, ancak yine de dosyanın bir ucunu belirtmemiz gerekir. Bu nedenle, her seferinde bir seçeneğimiz var, iki değer döndürelim (bayt değeri ve geçerli bir bayrak) veya başka bir şey olmadığında bir istisna oluşturun. İki durumda tüketen kod şöyle görünebilir:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

alternatif olarak:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Ancak bu, PEP 343 uygulandığından ve geri taşındığından, withifadeye düzgün bir şekilde sarıldı . Yukarıdakiler, çok pitonik hale gelir:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

Python3'te bu:

for val in open(source, 'rb').read():
     processbyte(val)

Sizi arka plan, gerekçe, örnekler vb. Veren PEP 343'ü okumanızı şiddetle tavsiye ediyorum .

Sonlandırmayı belirtmek için jeneratör fonksiyonlarını kullanırken işlemin sonunu belirtmek için bir istisna kullanmak da olağandır.

Arama örneğinizin neredeyse kesinlikle geriye doğru olduğunu eklemek isterim, bu tür işlevler jeneratörler olmalı, ilk çağrıda ilk eşleşmeyi döndürüyor, daha sonra bir sonraki eşleşmeyi döndüren ikame çağrılar ve başka eşleşme NotFoundolmadığında bir istisna yaratıyor .


"HAYIR! - genel olarak değil - istisnalar, tek bir kod sınıfı haricinde iyi akış kontrolü uygulaması olarak kabul edilmez" diyorsunuz. Bu empatik ifadenin mantığı nerede? Bu yanıtın yineleme örneğine çok az odaklandığı görülüyor. İnsanların istisnaları kullanmayı düşünebilecekleri birçok durum var. Bu diğer durumlarda bunu yapmanın problemi nedir?
Daniel Waltrip

@DanielWaltrip Basit bir programın ifdaha iyi olacağı durumlarda , istisnaların, genellikle çok geniş ölçüde kullanıldığı çeşitli programlama dillerinde çok fazla kod görüyorum . Hata ayıklama ve test etme ya da kod davranışınız hakkında akıl yürütme söz konusu olduğunda, istisnalara güvenmemek daha iyidir - bunlar istisna olmalıdır. Üretim kodunda, basit bir geri dönüş yerine bir atma / yakalama yoluyla dönen bir hesaplama, bu, hesaplama sıfır hata ile bir bölmeye çarptığında, hatanın meydana geldiği yerde çarpışma yerine rastgele bir değer döndürdüğü anlamına geldiğini gördüm. ayıklama.
Steve Barnes

Hey @SteveBarnes, sıfır hata yakalama örneğine bölme zayıf programlama gibi geliyor (istisnayı yeniden yükseltmeyen çok genel bir yakalama ile).
wTyeRogers

Cevabınız şöyle kaynıyor: A) "HAYIR!" B) İstisnalar yoluyla kontrol akışının alternatiflerden daha iyi çalıştığı durumların geçerli örnekleri vardır C) Jeneratörleri kullanmak için sorunun kodunun düzeltilmesi (kontrol akışı olarak istisnalar kullanan ve bunu iyi yapan). Cevabınız genellikle bilgilendirici olsa da, koyu ve görünüşte birincil ifade olarak, bazı destek beklediğim A maddesini desteklemek için hiçbir argüman yok gibi görünüyor. Eğer desteklenirse, cevabınızı küçümsemem. Alas ...
wTyeRogers
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.