Buggy Python Kodu: Python Geliştiricilerinin Yaptığı En Yaygın 10 Hata

Yayınlanan: 2022-03-11

Python hakkında

Python, dinamik semantik ile yorumlanmış, nesne yönelimli, üst düzey bir programlama dilidir. Dinamik yazma ve dinamik bağlama ile birleştirilmiş yüksek düzeyde yerleşik veri yapıları, onu Hızlı Uygulama Geliştirme için ve ayrıca mevcut bileşenleri veya hizmetleri bağlamak için bir komut dosyası oluşturma veya tutkal dili olarak kullanım için çok çekici kılmaktadır. Python, modülleri ve paketleri destekler, böylece program modülerliğini ve kodun yeniden kullanımını teşvik eder.

Bu makale hakkında

Python'un basit, öğrenmesi kolay sözdizimi, Python geliştiricilerini - özellikle de dilde daha yeni olanları - bazı inceliklerini gözden kaçırmaya ve çeşitli Python dilinin gücünü hafife almaya yönlendirebilir.

Bunu akılda tutarak, bu makale, arkadaki bazı daha gelişmiş Python geliştiricilerini bile ısırabilecek, biraz incelikli, yakalanması daha zor hataların "ilk 10" listesini sunar.

(Not: Bu makale, dilde daha yeni olanlara yönelik olan Python Programcılarının Ortak Hatalarından daha ileri düzeyde bir hedef kitleye yöneliktir.)

Yaygın Hata #1: İşlev bağımsız değişkenleri için ifadeleri varsayılan olarak yanlış kullanma

Python, bir işlev bağımsız değişkeni için varsayılan bir değer sağlayarak isteğe bağlı olduğunu belirtmenize olanak tanır. Bu, dilin harika bir özelliği olsa da, varsayılan değer mutable olduğunda bazı karışıklıklara yol açabilir. Örneğin, şu Python işlev tanımını göz önünde bulundurun:

 >>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar

Yaygın bir hata, isteğe bağlı bağımsız değişken için bir değer sağlamadan işlev her çağrıldığında isteğe bağlı bağımsız değişkenin belirtilen varsayılan ifadeye ayarlanacağını düşünmektir. Örneğin, yukarıdaki kodda, foo() öğesinin tekrar tekrar çağrılmasının (yani, bir bar argümanı belirtmeden) her zaman 'baz' döndürmesi beklenebilir, çünkü varsayım, foo() her çağrıldığında (bir bar olmadan) olacağı varsayımıdır. belirtilen argüman) bar [] olarak ayarlanır (yani, yeni bir boş liste).

Ama bunu yaptığınızda gerçekte ne olduğuna bir bakalım:

 >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]

Ha? Neden her seferinde yeni bir liste oluşturmak yerine, foo() her çağrıldığında var olan bir listeye varsayılan "baz" değerini eklemeye devam etti?

Daha gelişmiş Python programlama yanıtı, bir işlev argümanı için varsayılan değerin, işlevin tanımlandığı anda yalnızca bir kez değerlendirilmesidir. Bu nedenle, bar argümanı yalnızca foo() ilk tanımlandığında varsayılan değerine (yani boş bir liste) başlatılır, ancak daha sonra foo() öğesine yapılan çağrılar (yani, bir bar argümanı belirtilmeden) aynı listeyi kullanmaya devam eder. hangi bar başlangıçta başlatıldı.

Bilginize, bunun için ortak bir geçici çözüm aşağıdaki gibidir:

 >>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]

Yaygın Hata #2: Sınıf değişkenlerini yanlış kullanmak

Aşağıdaki örneği göz önünde bulundurun:

 >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1

Mantıklı.

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

Evet, yine beklendiği gibi.

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

Ne $%#!& ?? Sadece Ax değiştirdik. Neden Cx de değişti?

Python'da sınıf değişkenleri dahili olarak sözlükler olarak işlenir ve genellikle Yöntem Çözünürlük Sırası (MRO) olarak adlandırılan şeyi takip eder. Bu nedenle, yukarıdaki kodda, x özniteliği C sınıfında bulunmadığından, temel sınıflarında aranacaktır (yukarıdaki örnekte yalnızca A , ancak Python birden çok kalıtımı desteklemesine rağmen). Başka bir deyişle, C A bağımsız olarak kendi x özelliği yoktur. Bu nedenle, Ax yapılan referanslar aslında Cx yapılan referanslardır. Bu, düzgün bir şekilde ele alınmadığı sürece bir Python sorununa neden olur. Python'daki sınıf nitelikleri hakkında daha fazla bilgi edinin.

Yaygın Hata #3: Bir istisna bloğu için parametreleri yanlış belirtme

Aşağıdaki koda sahip olduğunuzu varsayalım:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range

Buradaki sorun, except ifadesinin bu şekilde belirtilen istisnaların bir listesini almamasıdır . Bunun yerine, Python 2.x'te, except Exception, e sözdizimi, daha fazla inceleme için uygun hale getirmek için istisnayı belirtilen isteğe bağlı ikinci parametreye (bu durumda e ) bağlamak için kullanılır. Sonuç olarak, yukarıdaki kodda IndexError istisnası except ifadesi tarafından yakalanmıyor ; bunun yerine istisna, IndexError adlı bir parametreye bağlı olarak sona erer.

Bir except ifadesinde birden çok istisnayı yakalamanın doğru yolu, ilk parametreyi, yakalanacak tüm istisnaları içeren bir tanımlama grubu olarak belirtmektir. Ayrıca, maksimum taşınabilirlik için as anahtar sözcüğünü kullanın, çünkü bu sözdizimi hem Python 2 hem de Python 3 tarafından desteklenir:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

Yaygın Hata #4: Python kapsam kurallarının yanlış anlaşılması

Python kapsam çözünürlüğü, Yerel, Çevreleyen , Küresel, Yerleşik anlamına gelen LEGB kuralı olarak bilinen kuralı temel alır. Yeterince basit görünüyor, değil mi? Aslında, bunun Python'da çalışma şeklinin bazı incelikleri var, bu da bizi aşağıdaki yaygın, daha gelişmiş Python programlama problemine getiriyor. Aşağıdakileri göz önünde bulundur:

 >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment

Sorun ne?

Yukarıdaki hata, bir kapsamdaki bir değişkene atama yaptığınızda, bu değişkenin Python tarafından otomatik olarak o kapsamda yerel olarak kabul edilmesi ve herhangi bir dış kapsamda benzer şekilde adlandırılmış herhangi bir değişkeni gölgelemesi nedeniyle oluşur.

Bu nedenle birçoğu, bir işlevin gövdesinde bir yere bir atama ifadesi eklenerek değiştirildiğinde, daha önce çalışan kodda bir UnboundLocalError . (Bunun hakkında daha fazla bilgiyi buradan okuyabilirsiniz.)

Bunun, listeleri kullanırken geliştiricileri tetiklemesi özellikle yaygındır. Aşağıdaki örneği göz önünde bulundurun:

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment

Ha? foo1 sorunsuz çalışırken neden foo2 bombaladı?

Cevap, önceki örnek problemdekiyle aynıdır, ancak kuşkusuz daha inceliklidir. foo1 lst öğesine atama yapmıyor, oysa foo2 . lst += [5] öğesinin gerçekten lst = lst + [5] için kısa yol olduğunu hatırlayarak, lst öğesine bir değer atamaya çalıştığımızı görürüz (bu nedenle Python tarafından yerel kapsamda olduğu varsayılır). Ancak, lst atamak istediğimiz değer, henüz tanımlanmamış olan lst kendisine dayanmaktadır (yine şimdi yerel kapsamda olduğu varsayılmaktadır). Boom.

Yaygın Hata #5: Üzerinde yinelenirken bir listeyi değiştirmek

Aşağıdaki kodla ilgili sorun oldukça açık olmalıdır:

 >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range

Bir listeden veya diziden bir öğeyi yinelerken silmek, deneyimli yazılım geliştiricilerin iyi bildiği bir Python sorunudur. Ancak yukarıdaki örnek oldukça açık olsa da, ileri düzey geliştiriciler bile çok daha karmaşık olan bu kod tarafından istemeden de olsa ısırılabilir.

Neyse ki Python, doğru kullanıldığında önemli ölçüde basitleştirilmiş ve akıcı kodla sonuçlanabilecek bir dizi zarif programlama paradigması içerir. Bunun bir yan yararı, daha basit kodun, bir liste öğesinin yanlışlıkla silinmesi sırasında yineleme hatası tarafından ısırılma olasılığının daha düşük olmasıdır. Böyle bir paradigma, liste kavrayışlarıdır. Ayrıca, liste kavrayışları, yukarıdaki kodun mükemmel çalışan bu alternatif uygulamasında gösterildiği gibi, bu özel sorundan kaçınmak için özellikle yararlıdır:

 >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]

Yaygın Hata #6: Python'un değişkenleri kapanışlarda nasıl bağladığını karıştırmak

Aşağıdaki örnek göz önüne alındığında:

 >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...

Aşağıdaki çıktıyı bekleyebilirsiniz:

 0 2 4 6 8

Ama aslında şunu elde edersiniz:

 8 8 8 8 8

Sürpriz!

Bu, Python'un kapanışlarda kullanılan değişkenlerin değerlerinin iç fonksiyon çağrıldığında arandığını söyleyen geç bağlama davranışı nedeniyle olur. Bu nedenle, yukarıdaki kodda, döndürülen işlevlerden herhangi biri çağrıldığında, çağrıldığı anda çevreleyen kapsamda i değeri aranır (ve o zamana kadar döngü tamamlanmıştır, bu nedenle i son işlevi zaten atanmıştır). 4) değeri.

Bu yaygın Python sorununun çözümü biraz zor:

 >>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8

işte! İstenen davranışı elde etmek için anonim işlevler oluşturmak için burada varsayılan argümanlardan yararlanıyoruz. Bazıları buna zarif diyebilir. Bazıları buna incelik diyebilir. Bazıları bundan nefret eder. Ancak bir Python geliştiricisiyseniz, her durumda anlamak önemlidir.

Yaygın Hata #7: Döngüsel modül bağımlılıkları oluşturma

Diyelim ki her biri diğerini içe b.py a.py iki dosyanız var:

a.py :

 import b def f(): return bx print f()

Ve b.py :

 import a x = 1 def g(): print af()

İlk önce, a.py içe aktarmayı deneyelim:

 >>> import a 1

İyi çalıştı. Belki de bu sizi şaşırtıyor. Sonuçta, burada muhtemelen bir sorun olması gereken dairesel bir içe aktarmamız var, değil mi?

Cevap, yalnızca dairesel bir içe aktarmanın varlığının Python'da başlı başına bir sorun olmadığıdır. Bir modül zaten içe aktarılmışsa, Python onu yeniden içe aktarmaya çalışmayacak kadar akıllıdır. Ancak, her modülün diğerinde tanımlanan işlevlere veya değişkenlere erişmeye çalıştığı noktaya bağlı olarak, gerçekten sorunlarla karşılaşabilirsiniz.

Örneğimize dönersek, a.py dosyasını içe aktardığımızda, a.py içe aktarılırken a.py öğesinden hiçbir şeyin tanımlanmasını gerektirmediğinden, b.py b.py aktarırken sorun a.py . b.py a tek başvuru, af() çağrısıdır. Ancak bu çağrı g() içindedir ve a.py veya a.py içindeki hiçbir şey g g() ) öğesini b.py . Yani hayat güzel.

Ancak, b.py içe aktarmaya çalışırsak ne olur (önceden a.py dosyasını içe a.py , yani):

 >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'

Ah-oh. Bu iyi değil! Buradaki sorun, b.py içe aktarma sürecinde, a.py öğesini içe aktarmaya çalışmasıdır, bu da f() öğesini çağırır ve bx a.py erişmeye çalışır. Ancak bx henüz tanımlanmadı. Bu nedenle AttributeError istisnası.

Buna en az bir çözüm oldukça önemsizdir. a.py g() içinde içe aktarmak için b.py değiştirmeniz yeterlidir:

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

Hayır, içe aktardığımızda her şey yolunda:

 >>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'

Yaygın Hata #8: Python Standart Kitaplığı modülleriyle ad çakışması

Python'un güzelliklerinden biri, "kutudan çıktığı gibi" gelen kitaplık modüllerinin zenginliğidir. Ancak sonuç olarak, bilinçli olarak bundan kaçınmıyorsanız, modüllerinizden birinin adı ile Python ile birlikte gelen standart kitaplıkta aynı ada sahip bir modül arasında bir ad çakışmasıyla karşılaşmak o kadar da zor değil (örneğin , email.py adında bir modül olabilir ve bu aynı ada sahip standart kitaplık modülüyle çakışabilir).

Bu, sırayla bir modülün Python Standart Kitaplığı sürümünü içe aktarmaya çalışan başka bir kitaplığı içe aktarmak gibi bunaltıcı sorunlara yol açabilir, ancak aynı ada sahip bir modülünüz olduğundan, diğer paket yanlışlıkla içindeki sürüm yerine sürümünüzü içe aktarır. Python Standart Kitaplığı. Kötü Python hatalarının gerçekleştiği yer burasıdır.

Bu nedenle, Python Standart Kitaplığı modüllerindeki adlarla aynı adları kullanmaktan kaçınmaya özen gösterilmelidir. Paketinizdeki bir modülün adını değiştirmeniz, bir Python Geliştirme Teklifi (PEP) dosyalamaktan ve bir ad değişikliği yukarı yönde talep etmekten ve bunu deneyip onaylatmaktan çok daha kolaydır.

Yaygın Hata #9: Python 2 ve Python 3 arasındaki farkları ele almamak

Aşağıdaki foo.py dosyasını göz önünde bulundurun:

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()

Python 2'de bu iyi çalışır:

 $ python foo.py 1 key error 1 $ python foo.py 2 value error 2

Ama şimdi Python 3'te bir girdap yapalım:

 $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

Az önce burada ne oldu? "Sorun", Python 3'te istisna nesnesine istisna bloğunun kapsamı except erişilebilir olmamasıdır. (Bunun nedeni, aksi takdirde, çöp toplayıcı çalışana ve referansları bellekten temizleyene kadar yığın çerçevesi ile bir referans döngüsünü bellekte tutacaktır. Bununla ilgili daha fazla teknik ayrıntı burada mevcuttur).

Bu sorunu önlemenin bir yolu, erişilebilir durumda kalması için istisna nesnesine bir başvuruyu except bloğunun kapsamı dışında tutmaktır. İşte bu tekniği kullanan önceki örneğin bir versiyonu, böylece hem Python 2 hem de Python 3 dostu kod veriyor:

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()

Bunu Py3k'de çalıştırmak:

 $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2

Yippee!

(Bu arada, Python İşe Alma Kılavuzumuz, kodu Python 2'den Python 3'e taşırken dikkat edilmesi gereken bir dizi başka önemli farkı tartışıyor.)

Yaygın Hata #10: __del__ yöntemini yanlış kullanmak

Diyelim ki bunu mod.py adlı bir dosyada buldunuz:

 import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)

Ve sonra bunu another_mod.py yapmaya çalıştınız:

 import mod mybar = mod.Bar()

Çirkin bir AttributeError istisnası alırsınız.

Niye ya? Çünkü burada bildirildiği gibi yorumlayıcı kapandığında, modülün global değişkenlerinin tümü None olarak ayarlanır. Sonuç olarak, yukarıdaki örnekte, __del__ öğesinin çağrıldığı noktada, foo adı zaten None olarak ayarlanmıştır.

Bu biraz daha gelişmiş Python programlama sorununa bir çözüm, bunun yerine atexit.register() kullanmak olabilir. Bu şekilde, programınızın çalışması bittiğinde (yani normal olarak çıkarken), yorumlayıcı kapatılmadan önce kayıtlı işleyicileriniz başlatılır.

Bu anlayışla, yukarıdaki mod.py kodu için bir düzeltme şöyle görünebilir:

 import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)

Bu uygulama, normal program sonlandırıldığında gerekli temizleme işlevlerini çağırmanın temiz ve güvenilir bir yolunu sağlar. Açıkçası, self.myhandle adına bağlı nesneyle ne yapılacağına karar vermek foo.cleanup kalmış, ancak siz self.myhandle .

Sarmak

Python, üretkenliği büyük ölçüde artırabilecek birçok mekanizma ve paradigmaya sahip güçlü ve esnek bir dildir. Bununla birlikte, herhangi bir yazılım aracı veya dilinde olduğu gibi, yeteneklerinin sınırlı bir şekilde anlaşılması veya takdir edilmesi, bazen bir faydadan çok bir engel olabilir ve kişiyi meşhur “tehlikeli olacak kadar bilmek” durumunda bırakır.

Bu makalede bahsedilen orta düzeyde gelişmiş programlama sorunları gibi (ancak bunlarla sınırlı olmayan) Python'un temel nüansları hakkında bilgi sahibi olmak, daha yaygın olan bazı hatalardan kaçınırken dilin kullanımını optimize etmeye yardımcı olacaktır.

Python uzmanlarının belirlenmesine yardımcı olabilecek mülakat sorularıyla ilgili öneriler için Insider's Guide to Python Röportajına da göz atmak isteyebilirsiniz.

Bu makaledeki işaretçileri yararlı bulduğunuzu umuyoruz ve geri bildiriminizi bekliyoruz.