Python е интерпретиран, обектно-ориентиран език за програмиране на високо ниво с динамична семантика. Вградените в него структури от данни на високо ниво, съчетани с динамично въвеждане и динамично свързване, го правят много привлекателен за Бързо разработване на приложения , както и за използване като скриптов или лепилен език за свързване на съществуващи компоненти или услуги. Python поддържа модули и пакети, като по този начин насърчава модулността на програмата и повторното използване на кода.
Простият, лесен за усвояване синтаксис на Python може да подведе Разработчици на Python - особено тези, които са по-нови от езика - да пропуснат някои от неговите тънкости и да подценят силата на разнообразен език на Python .
Имайки това предвид, тази статия представя списък с „топ 10“ с малко фини, по-трудни за улавяне грешки, които могат да хапят дори още напреднали разработчици на Python отзад.
(Забележка: Тази статия е предназначена за по-напреднала аудитория от Често срещани грешки на програмистите на Python , което е насочено повече към по-новите в езика.)
Python ви позволява да посочите, че аргументът на функцията е по избор чрез предоставяне на a стойност по подразбиране за него. Въпреки че това е чудесна характеристика на езика, това може да доведе до известно объркване, когато стойността по подразбиране е променлив . Например, помислете за тази дефиниция на функцията на Python:
>>> 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
Често срещана грешка е да мислим, че незадължителният аргумент ще бъде зададен на посочения израз по подразбиране всеки път функцията се извиква без предоставяне на стойност за незадължителния аргумент. Например в горния код може да се очаква, че извикването foo()
многократно (т.е. без да се посочва аргумент bar
) винаги би връщало 'baz'
, тъй като предположението би било, че всеки път foo()
се извиква (без да е посочен bar
аргумент) bar
е зададено на []
(т.е. нов празен списък).
Но нека да разгледаме какво всъщност се случва, когато направите това:
>>> foo() ['baz'] >>> foo() ['baz', 'baz'] >>> foo() ['baz', 'baz', 'baz']
А? Защо продължи да добавя стойността по подразбиране на 'baz'
до един съществуващи списък всеки път foo()
беше извикан, вместо да създава ново списък всеки път?
По-напредналият отговор за програмиране на Python е този стойността по подразбиране за аргумент на функция се оценява само веднъж, по времето, когато функцията е дефинирана. По този начин, bar
аргументът се инициализира по подразбиране (т.е. празен списък) само когато foo()
първо се дефинира, но след това се извиква към foo()
(т.е. без посочен аргумент bar
) ще продължи да използва същия списък, към който bar
е първоначално инициализиран.
FYI, често срещано решение за това е следното:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append('baz') ... return bar ... >>> foo() ['baz'] >>> foo() ['baz'] >>> foo() ['baz']
Обмислете следния пример:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1
Има смисъл.
>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1
Да, отново, както се очакваше.
>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3
Какво $% #! & ?? Променихме само A.x
. Защо C.x
промяна също?
В Python променливите на класа се обработват вътрешно като речници и следват това, което често се нарича Решение за разрешаване на метод (MRO) . Така че в горния код, тъй като атрибутът x
не е намерен в клас C
, той ще бъде търсен в основните си класове (само A
в горния пример, въпреки че Python поддържа множество наследства). С други думи, C
няма собствен x
собственост, независима от A
. По този начин препратките към C.x
всъщност са препратки към A.x
. Това причинява проблем с Python, освен ако не се обработва правилно. Научете повече за атрибути на клас в Python .
Да предположим, че имате следния код:
>>> try: ... l = ['a', 'b'] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File '', line 3, in IndexError: list index out of range
Проблемът тук е, че except
изявление прави не вземете списък с изключения, посочени по този начин. По-скоро в Python 2.x синтаксисът except Exception, e
се използва за обвързване на изключението с по избор посочен втори параметър (в случая e
), за да бъде предоставен за по-нататъшна проверка. В резултат на това в горния код IndexError
изключение е не уловени от except
изявление; по-скоро изключението вместо това завършва с обвързване с параметър, наречен IndexError
.
Правилният начин за улавяне на множество изключения в except
израз е да се посочи първият параметър като a кортеж съдържащ всички изключения, които трябва да бъдат уловени. Също така, за максимална преносимост, използвайте as
ключова дума, тъй като този синтаксис се поддържа както от Python 2, така и от Python 3:
>>> try: ... l = ['a', 'b'] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Разделителната способност на Python се основава на това, което е известно като LEGB правило, което е стенография за L окален, Е затваряне, G лобален, Б. вграден. Изглежда достатъчно директно, нали? Е, всъщност има някои тънкости в начина, по който това работи в Python, което ни отвежда до общия по-усъвършенстван проблем за програмиране на Python по-долу. Помислете за следното:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File '', line 1, in File '', line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
Какъв е проблема?
Горната грешка възниква, защото, когато направите възлагане към променлива в обхват, тази променлива автоматично се счита от Python за локална за този обхват и засенчва всяка подобно наречена променлива във всеки външен обхват.
най-доброто място за учене c
По този начин мнозина са изненадани да получат UnboundLocalError
в по-рано работещ код, когато е модифициран чрез добавяне на оператор за присвояване някъде в тялото на функция. (Можете да прочетете повече за това тук .)
Особено често се случва това да задейства разработчиците при използване списъци . Обмислете следния пример:
>>> 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 '', line 1, in File '', line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
А? Защо foo2
бомба докато foo1
работи добре?
Отговорът е същият като в предишния примерен проблем, но несъмнено е по-фин. foo1
не прави възлагане до lst
, докато foo2
е. Спомняйки си, че lst += [5]
всъщност е просто стенография за lst = lst + [5]
, виждаме, че се опитваме възлага стойност до lst
(следователно се предполага от Python, че е в локалния обхват). Стойността, която обаче искаме да присвоим на lst
се основава на lst
самата (отново, сега се предполага, че е в локалния обхват), която все още не е дефинирана. Бум.
Проблемът със следния код трябва да е доста очевиден:
>>> 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 '', line 2, in IndexError: list index out of range
Изтриването на елемент от списък или масив, докато се итерира върху него, е проблем на Python, който е добре известен на всеки опитен разработчик на софтуер. Но макар че горният пример може да е доста очевиден, дори напредналите разработчици могат да бъдат непреднамерено ухапани от това в много по-сложен код.
За щастие, Python включва редица елегантни програмни парадигми, които, когато се използват правилно, могат да доведат до значително опростен и рационализиран код. Странична полза от това е, че е по-малко вероятно по-простият код да бъде ухапан от случайно изтриване на списък-елемент-докато-итерация над него. Една такава парадигма е тази на разбиране на списъка . Освен това разбирането на списъци е особено полезно за избягване на този специфичен проблем, както е показано от това алтернативно изпълнение на горния код, който работи перфектно:
>>> 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]
Имайки предвид следния пример:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
Може да очаквате следния изход:
0 2 4 6 8
Но всъщност получавате:
8 8 8 8 8
Изненада!
Това се случва поради Python’s късно обвързване поведение, което казва, че стойностите на променливите, използвани в затварянията, се търсят в момента на извикване на вътрешната функция. Така че в горния код, когато се извика някоя от върнатите функции, стойността на i
е погледнато нагоре в околния обхват по времето, когато се нарича (и дотогава цикълът е завършен, така че i
вече е присвоена крайната му стойност 4).
Решението на този често срещан проблем с Python е малко хак:
>>> 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
Voilà! Тук се възползваме от аргументите по подразбиране, за да генерираме анонимни функции, за да постигнем желаното поведение. Някои биха нарекли това елегантно. Някои биха го нарекли фин. Някои го мразят. Но ако сте разработчик на Python, важно е да разберете във всеки случай.
Да приемем, че имате два файла, a.py
и b.py
, всеки от които внася другия, както следва:
В a.py
:
import b def f(): return b.x print f()
И в b.py
:
import a x = 1 def g(): print a.f()
Първо, нека опитаме да импортираме a.py
:
>>> import a 1
Работеше добре. Може би това ви изненадва. В края на краищата тук имаме кръгов внос, който вероятно би трябвало да е проблем, нали?
Отговорът е, че просто присъствие на циркулярно импортиране само по себе си не е проблем в Python. Ако модул вече е импортиран, Python е достатъчно умен, за да не се опитва да го импортира отново. Въпреки това, в зависимост от точката, в която всеки модул се опитва да получи достъп до функции или променливи, дефинирани в другия, наистина може да срещнете проблеми.
Така че връщайки се към нашия пример, когато импортирахме a.py
, нямаше проблем с импортирането b.py
, тъй като b.py
не изисква нищо от a.py
да бъдат определени по времето, когато се внася . Единствената препратка в b.py
до a
е повикването към a.f()
. Но това обаждане е в g()
и нищо в a.py
или b.py
извиква g()
. Така че животът е добър.
Но какво се случва, ако се опитаме да импортираме b.py
(без предварително импортирани a.py
, т.е.):
>>> import b Traceback (most recent call last): File '', line 1, in File 'b.py', line 1, in import a File 'a.py', line 6, in print f() File 'a.py', line 4, in f return b.x AttributeError: 'module' object has no attribute 'x'
А-а. Това не е добре! Проблемът тук е, че в процеса на импортиране b.py
, той се опитва да импортира a.py
, което от своя страна извиква f()
, което се опитва да осъществи достъп b.x
. Но b.x
все още не е дефиниран. Следователно AttributeError
изключение.
Поне едно решение за това е доста тривиално. Просто модифицирайте b.py
за импортиране a.py
в рамките на g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print a.f()
Не, когато го импортираме, всичко е наред:
>>> import b >>> b.g() 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'
Една от красотите на Python е богатството на библиотечни модули, които се предлагат „извън кутията“. Но в резултат, ако не го избягвате съзнателно, не е толкова трудно да срещнете сблъсък между имената на един от вашите модули и модул със същото име в стандартната библиотека, която се доставя с Python (например , може да имате модул с име email.py
във вашия код, който би бил в конфликт със стандартния библиотечен модул със същото име).
Това може да доведе до груби проблеми, като импортиране на друга библиотека, която от своя страна се опитва да импортира версията на модул на Python Standard Library, но тъй като имате модул със същото име, другият пакет погрешно импортира вашата версия вместо тази в стандартната библиотека на Python. Тук се случват лоши грешки на Python.
какво е Adobe xd cc
Следователно трябва да се внимава да се избягва използването на същите имена като тези в модулите на Python Standard Library. За вас е много по-лесно да промените името на модул в пакета си, отколкото да подадете a Предложение за подобряване на Python (PEP) да поискате промяна на името нагоре по веригата и да опитате да получите това одобрение.
Помислете за следния файл foo.py
:
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 това работи добре:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Но сега нека го завъртим на Python 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File 'foo.py', line 19, in bad() File 'foo.py', line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
Какво се е случило тук? „Проблемът“ е, че в Python 3 обектът за изключение не е достъпен извън обхвата на except
блок. (Причината за това е, че в противен случай ще запази референтен цикъл с рамката на стека в паметта, докато събирачът на боклук не стартира и не изчисти референциите от паметта. Налични са повече технически подробности за това тук ).
Един от начините да се избегне този проблем е да се поддържа препратка към обекта за изключение отвън обхвата на except
блок, така че да остане достъпен. Ето версия на предишния пример, която използва тази техника, като по този начин се получава код, който е както за Python 2, така и за Python 3:
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()
Стартиране на това на Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Йипи!
(Между другото, нашата Ръководство за наемане на Python обсъжда редица други важни разлики, които трябва да знаете, когато мигрирате код от Python 2 към Python 3.)
__del__
методДа приемем, че сте имали това във файл, наречен mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
И след това се опитахте да направите това от another_mod.py
:
import mod mybar = mod.Bar()
Ще получите грозно AttributeError
изключение.
Защо? Защото, както се съобщава тук , когато интерпретаторът се изключи, глобалните променливи на модула са настроени на None
В резултат на това в горния пример, в точката, която __del__
се извиква, името foo
вече е зададено на None
.
Решение на този малко по-напреднал проблем с програмирането на Python би било да се използва atexit.register()
вместо. По този начин, когато вашата програма приключи с изпълнението (когато излиза нормално, т.е.), вашите регистрирани манипулатори се стартират преди преводачът е изключен.
С това разбиране, поправка за горното mod.py
кодът може да изглежда по следния начин:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Това изпълнение осигурява чист и надежден начин за извикване на необходимата функционалност за почистване при нормално прекратяване на програмата. Очевидно е, че зависи до foo.cleanup
да решите какво да правите с обекта, обвързан с името self.myhandle
, но вие разбирате идеята.
Python е мощен и гъвкав език с много механизми и парадигми, които могат значително да подобрят производителността. Както при всеки софтуерен инструмент или език, обаче, ограниченото разбиране или оценка на неговите възможности понякога може да бъде по-скоро пречка, отколкото полза, оставяйки човек в пословичното състояние на „да знае достатъчно, за да бъде опасен“.
Запознаването с ключовите нюанси на Python, като (но в никакъв случай не само) средно напредналите проблеми с програмирането, повдигнати в тази статия, ще помогне да се оптимизира използването на езика, като същевременно се избегнат някои от най-често срещаните му грешки.
Може да искате да разгледате и нашите Ръководство на Insider за интервюиране с Python за предложения по въпроси за интервю, които могат да помогнат за идентифицирането на експертите на Python.
Надяваме се, че сте намерили указанията в тази статия за полезни и ще приветстваме вашите отзиви.