Python е обектно-ориентиран и интерпретиран програмен език на високо ниво с динамична семантика. Неговото високо ниво на интегрирани структури от данни, съчетано с динамично писане и подвързване го правят много привлекателен за бърза разработка на приложения , както и за използване като скриптов език или лепило за свързване на съществуващи компоненти или услуги. Python работи с модули и пакети, като по този начин насърчава модулността на програмата и повторното използване на кода.
Лесен и лесен за научаване синтаксис на Python, можете да изпратите до разработчици на python в грешната посока - особено тези, които учат езика - губят част от неговите тънкости по пътя и подценяват силата на Различен език на Python .
Имайки предвид това, тази статия представя списък с „топ 10“ на фините, трудно забележими грешки, които могат да хванат дори някои от най-напредналите разработчици на Python.
( Забележка: Тази статия е предназначена за по-напреднала аудитория от често срещаните грешки на програмистите на Python, която е насочена повече към тези, които са нови за езика. )
Python ви позволява да посочите, че аргументът на функцията не е задължителен, като предоставя стойност по подразбиране за него. Въпреки че това е чудесна характеристика на езика, това може да доведе до известно объркване, когато е по подразбиране променлив . Например, помислете за тази дефиниция на функцията 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()
многократно (т.е. без да се посочва аргумент на лента) винаги би връщало baz
, тъй като хипотезата би била, че всеки път foo()
се извиква (без посочен аргумент на бара) bar се задава на []
(т.е. нов празен списък).
Но нека видим какво всъщност се случва, когато това се направи:
>>> foo() ['baz'] >>> foo() ['baz', 'baz'] >>> foo() ['baz', 'baz', 'baz']
Хей? Защо беше стойността по подразбиране на baz
към съществуващ списък всеки път че foo()
, вместо да създавате нов списък при всяка възможност? Най-напредналият отговор за програмиране на Python е, че стойността по подразбиране за аргумент на функция се оценява само веднъж, в момента, когато функцията е дефинирана. Следователно аргументът bar се инициализира до стойността си по подразбиране (т.е. празен списък) само когато foo()
е дефиниран първо, но след това извиква на foo()
(т.е. без посочен bar
аргумент) те пак ще използват същия списък като bar
е първоначално инициализиран.
Между другото, общо решение за това е следното:
>>> 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
.
API за местоположение на услугите на google play
Правилният начин за улавяне на множество изключения в отчет except
е да се посочи първият параметър като a двойно 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
Какъв е проблемът?
Горната грешка се дължи на факта, че когато a възлагане към променлива в обхват, тази променлива се счита автоматично от Python като локална в този обхват и следва всяка променлива с подобно име, във всеки външен обхват.
Затова много от тях са изненадани да получат 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
Не прави а възлагане a 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 включва редица елегантни програмни парадигми, които при правилно използване могат да доведат до значително опростен и рационализиран код. Вторична полза от това е, че по-малко вероятно е по-прост код да бъде уловен от случайното изтриване на грешка в списъка-елемент-докато-итерация. Една такава парадигма е [разбиране на списъци] ((https://docs.python.org/2/tutorial/datastructures.html#tut-listcomps). От друга страна, разбирането на списъци е особено полезно за избягване на този специфичен проблем, тъй като показано в тази алтернативна реализация на кода, показан по-горе, който работи перфектно:
>>> 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, който казва, че стойностите на променливите, използвани в затварянията, се извличат по време на извикване на вътрешната функция. Така че в горния код, когато се извика някоя от върнатите функции, стойността на 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
не изисква никакви b.py
да се дефинира по време на вноса. Единствената препратка в b.py
a 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, но тъй като вече имате модул със същото име, другият пакет погрешно импортира вашата версия, вместо от този, който се намира в стандартната библиотека на Python, и тук се появяват най-сериозните грешки.
Следователно трябва да се внимава да се избягват използването на същите имена като стандартните модули на библиотеката на Python. За вас е много по-лесно да преименувате модул в пакета си, отколкото да представите предложение за подобрение на 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
(Причината за това е, че в противен случай би запазил референтен цикъл с рамката на стека в паметта, докато събирачът на боклук не стартира и не изчисти референциите от паметта. Налични са повече технически подробности за това тук ).
Един от начините за заобикаляне на този проблем е да се запази препратката към обекта за изключение извън обхвата на блока освен, за да остане достъпен. Ето версия на примера по-горе, която използва тази техника, поради което дозира кода и го прави по-съвместим с 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:
научете C++ по трудния начин
$ 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’s Guide за интервюиране с Python , за предложения по въпроси за интервю, които могат да помогнат за идентифицирането на експертите на Python.
Надяваме се, че съветите в тази статия ще ви бъдат полезни и оценяваме отзивите ви.