29.09.2010

Ідыёмы і эфектыўнасць Python

Original on jaynes.colorado.edu

Аўтар Rob Knight, праект Cogent

Што ідыёмы мне выкарыстоўваць, каб унесці свой код лягчэй чытаць?

Прачытайце “Python Cookbook”, асабліва ў першыя некалькі частак. Гэта выдатная крыніца добра напісаны код прыкладаў Python.

Зборкі радка як спіс і join ''. join у канцы. separator з’яўляецца радком, метад, названы ў separator, а не спіс. Выклік ад пусты радок злучае часткі без якіх-небудзь падзельнік, які з’яўляецца дзівацтвам Python і вельмі нечаканая на першы погляд. Гэта важна: радок будынак + квадратычнага часу, а не лінейная! Калі вы пазнаяце, адна мова, навучыцца гэтаму.

Дрэнна: for s in strings: result += s 
Правільна: result = ''.join(strings) 

Заўсёды выкарыстоўвайце магчымасці аб’екта, а не яго тып. Python гэта дынамічна тыпізаваная мова, вы павінны галоўным чынам ніколі не абыякава, аб’ект уяўляе сабою адмысловы тып тых пор, пакуль яна падтрымлівае сеткавы інтэрфейс. Гэта можа даць вам уражлівы палімарфізму бясплатна. Напрыклад, мой код для праверкі ці з’яўляецца радок сапраўдная на алфавіт выглядае наступным чынам:

for char in string: 
    if char not in alphabet: 
        raise ValueError, "Char %s not in alphabet %a" % (char, alphabet) 

Гэта не мае значэнні, алфавіт радкі, слоўнік, спіс, ці аб’ект, які я вызначаю як доўга, бо падтрымлівае __contains__ адмысловым метадам.

Выкарыстанне у магчымасці (можна перавызначыць __contains__ для падтрымкі, калі ў х у сінтаксісе і __iter__ да падтрымцы х у сінтаксіс у сваіх уласных класах). Пры гэтым Вашы заявы цэлым і паліморфных.

Лепш: for key in d: print key     #also works for arbitrary sequence 
Горш:  for key in d.keys(): print key #limited to objects with keys() 
Лепш: if key not in d: d[key] = [] 
Горш:  if not dict.has_key(key): d[key] = [] 

Нататка: вы павінны выкарыстоўваць d.keys (), калі вы жадаеце мутаваць у слоўнік.for key in d: del d[key]будзе паднімаць RuntimeError: dictionary changed size during iteration. Выкарыстоўвайце for key in d.keys(): del d[key], замест гэтага.

Выкарыстанне ціску, калі аб’ект павінен быць вызначанага тыпу. Калі х павінна быць радок для вашага кода на працу, то чаму б не назваць str (х), а не спрабаваць штосьці накшталт isinstance (str х)? Вы можаце спакаваць яго ў try/except вы не жадаеце, каб злавіць памылкі, і, верагодна, у канчатковым выніку рашэнне, якое значна больш агульнага, чым калі б вы паспрабавалі прадбачыць усе магчымыя варыянты.

Выкарыстоўвайце, if not x замест if x == 0 ці if x == "" or if x == None ці if x == False аналагічна, if замест х, калі х = 0, калі х! = Не, і г.д. . Выключэнне: для лікаў, 0 ілжывае значэнне, таму вам можа спатрэбіцца правесці адрозненне паміж 0 і іншыя рэчы, якія вяртаюць False. Сцеражыцеся параўнанні з плывучай кропкай, якая павінна быць нуля Int (0) ці float (0,0): памылкі акруглення, могуць выклікаць лютасць памылак.

Выкарыстанне радка метады, а не радок модуля. Напрыклад, можна выкарыстоўваць s.startswith(‘abc’), а не startswith(s, ‘abc’). Гэта дазваляе выкарыстоўваць іншыя аб’екты, якія толькі падтрымка невялікая частка радок інтэрфейсу (напрыклад, тыя, што вы пішаце самі): модуль радковыя функцыі звычайна чакаюць рэальных радкоў. Ёсць некалькі выпадкаў, калі вам трэба import string: напрыклад, maketrans па-ранейшаму даступная толькі праз радок модуля. Аднак, у цэлым, бачачы, што імпарт заява папераджальны знак.

Выкарыстоўвайце for line in infile, а не for line in infile.readlines(). readlines і xreadlinesз’яўляюцца састарэлымі ў Python 2.3 усё адно ў карысць новага пратаколу ітэратара.Па лініі for line in infile дазваляе INFILE быць усё, што дзейнічае як паслядоўнасць радкоў, напрыклад, спіс, які можа ў значнай ступені дапамогі тэставання. Насамрэч, проста выкарыстоўвайце for line in lines: вы павінны галоўным чынам ніколі не абыякава, лініі выходзяць з файл, спіс радкоў, іншы ітэратар, ключы слоўнік, ці любы іншы.

Каб адмяніць сартаванне спісу, выкарыстоўвайце:

list.sort() 
list.reverse() 

Гэта значна лягчэй чытаць і, дарэчы, хутчэй, чым хітрая-лайн альтэрнатывы 1. Памятаеце, што на месцы такія метады, як сартаваць () і зваротны () не вяртае значэнне. Гэта можа здацца дзіўным, таму што калі вы штосьці накшталт sorted_list = orig_list.sort() пры гэтым не паказваецца sorted_list orig_list і цяпер знаходзіцца ў вызначаным парадку. Заўважым, што калі вы проста жадаеце перабору зваротным спісе, вы можаце (у Python 2.5, прынамсі) выкарыстоўваць для for i in reversed(sorted(orig_list)).

Выкарыстоўвайце ‘while 1:’ за бясконцыя цыклы, ці заўсёды выканаць цела цыклу хоць бы адзін раз. Гэта проста мова Python, але гэта тое, што іншыя людзі будуць чакаць, калі яны абвыклі да мовы. Напрыклад:

while 1: 
    curr_line = reader.next() 
    if not curr_line: 
        break 
    curr_line.process()		



Шукайце памылкі, а не пазбягайце іх, каб пазбегнуць загрувашчванні код адмысловых выпадках. Гэта мова завецца EAFP (‘лягчэй прасіць прабачэнні, чым дазволы “), а супраць LBYL (‘сем разоў адмерай, адзін раз адрэж’). Гэта часта робіць код больш чытэльным. Напрыклад:

Горш: 
#check whether int conversion will raise an error 
if not isinstance(s, str) or not s.isdigit: 
    return None 
elif len(s) > 10:    #too many digits for int conversion 
    return None 
else: 
    return int(str)

Лепш: 
try: 
    return int(str) 
except (TypeError, ValueError, OverflowError): #int conversion failed 
    return None 

(Заўважым, што ў дадзеным выпадку, другі варыянт нашмат лепш, паколькі ён карэктна апрацоўвае кіроўных + і -, а таксама значэнні ад 2 да 10 млрд. дол. (для 32-разрадных машынах). Не бязладзіца код, прадбачачы ўсе магчымыя няўдачы: паспрабуйце яго і выкарыстоўваць адпаведную апрацоўку выключэнняў.)

Ловіце толькі адпаведныя памылкі. Гэта неверагодна рызыкоўна выкарыстоўваць лавіць без указання, якія памылкі вы жадаеце перахапляць, бо ён будзе атрымліваць усё. Калі вы чакаеце адмысловага роду памылкі, такія, як ZeroDivisionError ці ValueError, не лавіць усё астатняе, а таксама на здагадцы, што яны з’яўляюцца тымі адзінымі, якія могуць паўстаць. Вы можаце быць з памяці, а не, ці вы, магчыма, прайшлі ў аб’ект, які не мае правы атрыбут ці не выканаў працу. Маскіроўка гэтыя нечаканыя памылкі робіць адладкі вельмі цяжка, асабліва калі вы друкуеце якія ўводзяць у памылку паведамлення пра памылкі.

Змена значэнняў без выкарыстання часавых зменных. Замест гэтага варта выкарыстоўваць няяўныя картэж распакавання. Вы можаце напісаць, B = B, для замены і b. Сапраўды, вы можаце зрабіць гэта з такой колькасцю пунктаў, як вам падабаецца: A, B, C, D = D, B, C, на карту, каб D і т.д.

Выкарыстанне паштовы атрымаць (ці спісу паслядоўнасці) элементаў з іх індэксы:

			indices = xrange(maxint)    #only need this once; mine is in Utils.py 
			for d, index in zip(data, indices): 
			#do something with d and index here 

(Заўважым, што Python 2.3 падае пералічыць (дадзеных), якая прадугледжвае гультаяватыя вылічэнні паслядоўнасці і робіць гэта мова ў значнай ступені патрэбныя. Гэта можа быць карысна, калі вы жадаеце ўключыць індэкс разам з побач іншых спісаў, аднак, напрыклад, паштовы (list_1, list_2, індэксы). можа не спрацаваць на некаторых 64-бітных сістэм).

Калі вам не трэба індэксаў, выканаеце:

for i in items: 
    something(i) 
...rather than: 
for index in range(len(items)): 
    something(items[index]) 
		

(Больш машынапіс, выродлівей, і павольней.)

Якія метады лепш выкарыстоўваць, каб мой код працаваў хутчэй?

Заўсёды профіль да аптымізаваць па хуткасці. Заўсёды трэба аптымізаваць для чытання першая: лягчэй наладжваць чытаны код, чым чытаць “аптымізаваны” код, асабліва калі аптымізацыя не з’яўляюцца эфектыўнымі. Перш чым выкарыстоўваць любыя метады, што робіць код меней чытаным, вы павінны праверыць, што гэта сапраўды вузкае месца ў Вашай заяве, запусціўшы прыкладанне з дапамогай убудаванага ў profile.py сцэнар. Калі ваша праграма марнуе 10% часу працуе ў адмысловым парадку, нават калі павялічыць хуткасць дзесяць разоў вы толькі пагаліўся 9% ад агульнага часу працы.

Заўсёды выкарыстоўвайце добры алгарытм, калі яна будзе даступная. Выключэнне з гэтага правіла, калі Існуюць вядомыя вялікія адрозненні ў тэрмінах складанасці альтэрнатыўныя алгарытмы. Скарачэнне часу працы ад квадратычнага да лінейнай, экспанентнай ці да палінома, заўсёды варта рабіць, калі вы не ўпэўнены, што наборы дадзеных будуць заўсёды малюсенькія (менш, чым некалькі дзясяткаў найменняў).

Выкарыстоўвайце просты варыянт, які мог бы працаваць. Не выкарыстоўваць рэгулярныя выразы, калі вы проста жадаеце, каб, калі радок пачынаецца з падрадка прыватнасці: выкарыстоўваць. StartsWith замест. Не варта выкарыстоўваць .index калі вы проста жадаеце ўбачыць, калі радок не ўтрымоўвае пэўнага ліста: замест in. Не выкарыстоўвайце StringIO, калі вы маглі б проста выкарыстоўваць спіс радкоў. Увогуле, утрымліваючы яе прастой скароціць памылак і робіць ваш код больш чытэльным. Нават складаныя спалучэнні .index выклікі будуць значна хутчэй, чым рэгулярныя выразы, і, верагодна, лягчэй разабрацца, калі вы проста адпаведнасці, а не захопу вынік.

Зборкі радка як спіс і ''.join. у канцы. Так, вы ўжо бачылі гэта вышэй у частцы 1 “Python Ідыёмы”, але гэта такі важны, што я думаў, што гэта яшчэ раз. Далучыцца з’яўляецца радком, метад заклікаў сепаратара, а не спіс. Выклік ад пусты радок злучае часткі без якіх-небудзь падзельнік, які з’яўляецца дзівацтвам Python і вельмі нечаканая на першы погляд. Гэта важна: радок будынак + квадратычнага часу, а не лінейная!

Дрэнна:  
for s in strings: result += s 
Правільна:  
result = ''.join(strings)		



Выкарыстанне тэстаў для аб’екта асобы, калі гэта мэтазгодна: if x is not None, а if x != None. Гэта значна больш эфектыўна тэст-аб’ектаў для асобы, чым роўнасць, таму што асоба толькі правярае іх адрас у памяці (2 аб’екты ідэнтычныя, калі яны таго ж аб’екта ў тым жа фізічным месцы), а не іх фактычнымі дадзенымі.

Выкарыстанне слоўнікаў (ці наборы) для пошуку, а не спісы. Каб знайсці элементы агульнага паміж двума спісамі, зрабіць першы ў слоўніку, а затым шукаць элементы другой у ім. Пошук спіс пункта лінейнага часу, а пошук слоўнік ці набор для пункта сталай часу. Гэта часта дазваляе скараціць час пошуку ад квадратычнага да лінейнай.

Выкарыстанне ўбудаваных сартаваць па меры магчымасці. Сартаваць можа карыстацкай функцыі параўнання ў якасці параметру, але гэта робіць яго вельмі павольна, паколькі функцыя павінна быць названа прынамсі O(n log n) раз ва ўнутраным цыкле. Каб зэканоміць час, сваю чаргу, у пералік пунктаў, у спіс картэжаў, дзе першы элемент кожнага картэжа мае разліковае значэнне функцыі для кожнага элемента (напрыклад выманні поля), а апошні элемент сам элемент.

Гэта мова завецца DSU для decorate-sort-undecorate. У крок роду, выкарыстоўваючы ўбудаваны ў сартаванні па наборы. У “undecorate” крок, атрымаць зыходны спіс у вызначаным парадку, здабываючы апошні элемент з кожнага картэжа. Напрыклад:

aux_list = [i.Count, i.Name, ... i) for i in items] 
aux_list.sort()    #sorts by Count, then Name, ... , then by item itself 
sorted_list = [i[-1] for i in items] #extracts last item 

Для пазнейшых версіях Python, DSU часцяком няма неабходнасці. Гэта старонка мае добрыя абмеркаванні розных метадаў сартавання ў Python.

Выкарыстанне map і / ці ўжыць filter да функцыі ў спісы. map ужывае функцыю да кожнага элемента спісу (тэхнічна, паслядоўнасць), і вяртае спіс вынікаў. Фільтра ужывае функцыю да кожнага элемента ў паслядоўнасці, і вяртае спіс , які змяшчае толькі тыя элементы, для якіх функцыя адзнакі True (з выкарыстаннем __nonzero__ убудаваны метад). Гэтыя функцыі можна зрабіць код нашмат карацей. Акрамя таго, яны робяць гэта нашмат хутчэй, бо цыкл праходзіць цалкам на C API і ніколі не было ў цыкле зменных прывязкі да Python аб’ектаў.

Горш: 
strings = [] 
for d in data: 
    strings.append(str(d))

Лепш: 
strings = map(str, data) 

Выкарыстанне спіскавыя, дзе Ёсць умовы, злучаныя, ці там, дзе функцыі метадаў ці прымаць больш аднаго параметру. Гэтыя выпадкі, калі map і filter рабіць дрэнна, бо вы павінны зрабіць да 1 новы аргумент функцыі, што робіць аперацыю, якую Вы жадаеце . Гэта робіць іх значна павольней, бо больш праца вядзецца ў пласце Python. Спіскавыя часта нечакана для чытання.

Горш: 
result = [] 
for d in data: 
    if d.Count > 4: 
        result.append[3*d.Count]

Лепш: 
result = [3*d.Count for d in data if d.Count > 4] 
		

Калі вы выявілі, робячы той жа спіс разумення паўторна, каб функцыі карыснасці і выкарыстоўваць map і / ці filter:

def triple(x): 
    """Returns 3 * x.Count: raises AttributeError if .Count missing.""" 
    return 3 * x.Count 
def check_count(x): 
    """Returns 1 if x.Count exists and is greater than 3, 0 otherwise.""" 
    try: 
        return x.Count > 3 
    except: 
        return 0 
result = map(triple, filter(check_count, data)) 

Функцыя factories выкарыстоўваецца для стварэння дапаможных функцый. Часта, асабліва калі вы выкарыстоўваеце map і filter шмат, у Вас павінен дапаможныя функцыі, якія пераўтвораць іншых функцый ці метадаў з адным параметрам. У прыватнасці, вы часта жадаюць звязаць некаторыя дадзеныя функцыі адзін раз, а затым ужыць яе некалькі разоў для розных аб’ектаў. У прыведзеным вышэй прыкладзе, нам патрэбна функцыя, памножаная пэўнай вобласці аб’екта па 3, але тое, што мы сапраўды жадаем завод, які дазваляе арганізаваць абмен на любое імя поля і колькасць мультыплікатара ў гэтай сям’і:

def multiply_by_field(fieldname, multiplier): 
    """Returns function that multiplies field "fieldname" by multiplier.""" 
    def multiplier(x): 
        return getattr(x, fieldname) * multiplier 
    return multiplier 
triple = multiply_by_field('Count', 3) 
quadruple = multiply_by_field('Count', 4) 
halve_sum = multiply_by_field('Sum', 0.5) 
		

Гэта вельмі магутны і агульны метад атрымання функцыі, якія могуць рабіць штосьці накшталт пошуку вызначанай вобласці, каб атрымаць спіс слоў, ці выканаць шэраг мерапрыемстваў па розных абласцях пэўнага аб’екта, і г.д. Гэта боль, каб напісаць шмат маленькіх функцый што робяць вельмі падобныя рэчы, але калі яны вырабляюцца на заводзе функцыі гэта лёгка.

З дапамогай модуля аператара і reduce атрымаць сумы, прадуктаў і г.д. reduce прымае функцыі і паслядоўнасці. Спачатку ён ужывае функцыю да першых двух пунктаў, то яна прымае вынік і ўжывае функцыю да выніку, і да наступнага пункта, прымае гэтага выніку і ўжывае функцыю да яго і да наступнага пункта, і гэтак далей да канца спіс. Гэта робіць яго вельмі лёгка назапашваць пункты па спісе (ці, сапраўды, любой паслядоўнасці). Звернеце ўвагу, што Python 2.3 мае ўбудаваны ў суме () функцыі (толькі лічбы), што робіць меней неабходна, чым раней.

Горш: 
sum = 0 
for d in data: 
    sum += d 
product = 1 
for d in data: 
    product *= d

Лепш: 
from operator import add, mul 
sum = reduce(add, data) 
product = reduce(mul, data) 
		

Выкарыстанне zip і dict для адлюстравання палёў з імёнамі. ZIP апыняецца пары паслядоўнасцяў у спіс картэжаў, які змяшчае першы, другі і г.д. значэння з кожнай паслядоўнасці. Напрыклад, zip('abc', [1,2,3]) == [('a',1),('b',2),('c',3)]. Вы можаце выкарыстоўваць гэта, каб зэканоміць набраўшы пры наяўнасці поля ў вядомым парадку, што вы жадаеце адлюстраваць на імёны:

Дрэнна: 
fields = '|'.split(line) 
gi = fields[0] 
accession = fields[1] 
description = fields[2] 
#etc. 
lookup = {} 
lookup['GI'] = gi 
lookup['Accession'] = accession 
lookup['Description'] = description 
#etc.

Добра: 
fieldnames = ['GI', 'Accession', 'Description'] #etc. 
fields = '|'.split(line) 
lookup = dict(zip(fieldnames, fields))

Ідэальна: 
def FieldWrapper(fieldnames, delimiter, constructor=dict): 
    """Returns function that splits a line and wraps it into an object.

    Field names are passed in as keyword args, so constructor must be 
    expecting them as such. 
    """ 
    def FieldsToObject(line): 
        fields = [field.strip() for field in line.split(delimiter)] 
        result = constructor(**dict(zip(fieldnames, fields))) 
    return FieldsToObject

FastaFactory = FieldWrapper(['GI','Accession','Description'], '|', Fasta) 
TaxonFactory = FieldWrapper(['TaxonID', 'ParentID', ...], '|', Taxon) 
CodonFreqFactory = FieldWrapper(['UUU', 'UUC', 'UUA',...], ' ', CodonFreq) 
#etc for similar data, including any database tables you care to wrap 

Comments are closed.