Развивайте свои навыки Python

Напишите лучший код Python с помощью этих 10 приемов

Узнайте, как кодировать питоническим способом

Кодировать - это весело, а программировать на Python - еще веселее, потому что есть много разных способов реализовать одни и те же функции. Однако в большинстве случаев существуют предпочтительные реализации, которые некоторые люди называют Pythonic. Одной из общих черт этих реализаций Pythonic является аккуратность и лаконичность кода.

Программирование на Python или любом другом языке программирования - это не ракетостроение, а в основном ремесленные навыки. Если вы намеренно попробуете кодирование на Pythonic, эти методы скоро станут частью вашего набора инструментов, и вы обнаружите, что использовать их в своем проекте все более и более естественно. Итак, давайте рассмотрим некоторые из этих простых приемов, которые, я надеюсь, вам пригодятся.

1. Отрицательное индексирование

Людям нравится работать с последовательностями, потому что мы знаем порядок элементов и можем управлять этими элементами по порядку. В Python строки, кортежи и списки являются наиболее распространенными типами данных последовательности. Мы можем получить доступ к отдельным элементам с помощью индексации. Как и другие основные языки программирования, Python поддерживает индексирование на основе 0, при котором мы получаем доступ к первому элементу, используя ноль в квадратных скобках. Кроме того, мы также можем использовать объекты среза для извлечения определенных элементов последовательности, как показано в примерах кода ниже.

>>> # Positive Indexing
... numbers = [1, 2, 3, 4, 5, 6, 7, 8]
... print("First Number:", numbers[0])
... print("First Four Numbers:", numbers[:4])
... print("Odd Numbers:", numbers[::2])
... 
First Number: 1
First Four Numbers: [1, 2, 3, 4]
Odd Numbers: [1, 3, 5, 7]

Однако Python делает шаг вперед, поддерживая отрицательную индексацию. В частности, мы можем использовать -1 для ссылки на последний элемент в последовательности и подсчета элементов в обратном порядке. Например, предпоследний элемент имеет индекс -2 и так далее. Важно отметить, что отрицательная индексация также может работать с положительным индексом в объекте среза.

>>> # Negative Indexing
... data_shape = (100, 50, 4)
... names = ["John", "Aaron", "Mike", "Danny"]
... hello = "Hello World!"
... 
... print(data_shape[-1])
... print(names[-3:-1])
... print(hello[1:-1:2])
... 
4
['Aaron', 'Mike']
el ol

2. Проверьте пустоту контейнеров.

Контейнеры относятся к тем типам данных контейнеров, которые могут хранить другие данные. Некоторые часто используемые встроенные контейнеры - это кортежи, списки, словари и наборы. Когда мы имеем дело с этими контейнерами, нам часто необходимо проверить, содержат ли они какие-либо элементы, прежде чем выполнять дополнительные операции. Действительно, мы можем проверить длину этих контейнеров, которая соответствует количеству хранимых предметов. Когда длина равна нулю, контейнер пуст. Ниже показан тривиальный пример.

if len(some_list) > 0:
    # do something here when the list is not empty
else:
    # do something else when the list is empty

Однако это не самый питонический способ. Вместо этого мы можем просто проверить сам контейнер, который будет оценивать True, если он содержит элементы. Хотя в следующем коде показаны основные типы данных контейнера, такое использование также может применяться к строкам (т. Е. Любые непустые строки равны True).

>>> def check_container_empty(container):
...     if container:
...         print(f"{container} has elements.")
...     else:
...         print(f"{container} doesn't have elements.")
... 
... check_container_empty([1, 2, 3])
... check_container_empty(set())
... check_container_empty({"zero": 0, "one": 1})
... check_container_empty(tuple())
... 
[1, 2, 3] has elements.
set() doesn't have elements.
{'zero': 0, 'one': 1} has elements.
() doesn't have elements.

3. Создайте список строк с помощью Split ()

Мы часто используем строки в качестве идентификаторов для определенных объектов. Например, мы можем использовать строки для ключей в словаре. В проекте по науке о данных строки часто являются именами столбцов для данных. Когда мы выбираем несколько столбцов, нам неизбежно потребуется создать список строк. Действительно, мы можем создавать строки, используя литералы в списке. Однако нам придется написать пары кавычек, чтобы заключить каждую из строк, что довольно утомительно для нас, «ленивых». Таким образом, я предпочитаю создавать список строк, используя метод split() строки, как показано во фрагменте кода ниже.

>>> # List of strings
... # The typical way
... columns = ['name', 'age', 'gender', 'address', 'account_type']
... print("* Literals:", columns)
... 
... # Do this instead
... columns = 'name age gender address account_type'.split()
... print("* Split with spaces:", columns)
... 
... # If the strings contain spaces, you can use commas instead
... columns = 'name, age, gender, address, account type'.split(', ')
... print("* Split with commas:", columns)
... 
* Literals: ['name', 'age', 'gender', 'address', 'account_type']
* Split with spaces: ['name', 'age', 'gender', 'address', 'account_type']
* Split with commas: ['name', 'age', 'gender', 'address', 'account type']

Как показано выше, метод split() по умолчанию использует пробелы в качестве разделителей и создает список строк из строки. В частности, когда вы создаете список строк с некоторыми элементами, содержащими пробелы, вы можете дополнительно использовать другой тип разделителя (например, запятые).

Такое использование вдохновлено некоторыми встроенными функциями. Например, когда вы создаете именованный класс кортежа, мы можем сделать это: Student = namedtuple(“Student”, [“name”, “gender”, “age”]). Список строк определяет «атрибуты» кортежа. Однако он также изначально поддерживается за счет определения класса следующим образом: Student = namedtuple(“Student”, “name gender age”). В другом случае создание класса Enum поддерживает те же альтернативные решения.

4. Тернарное выражение

Во многих случаях нам необходимо определить переменные с конкретными значениями в зависимости от условий, и мы можем просто использовать оператор if… else для проверки условия. Однако для этого требуется несколько строк кода. Если мы имеем дело только с присваиванием одной переменной, мы можем использовать тернарное выражение, которое проверяет условие и завершает присвоение всего одной строкой кода. Кроме того, он имеет более короткую форму, что делает код еще более лаконичным. Рассмотрим следующий пример.

# The typical way
if score > 90:
    reward = "1000 dollars"
else:
    reward = "500 dollars"
# Do this instead
reward = "1000 dollars" if score > 90 else "500 dollars"

Иногда мы можем получить некоторые данные из определенной функции, и мы можем воспользоваться этим и написать сокращенную операцию тернарного выражения, как показано ниже.

# Another possible scenario
# You got a reward amount from somewhere else, but don't know if None/0 or not
reward = reward_known or "500 dollars"
# The above line of code is equivalent to below
reward = reward_known if reward_known else "500 dollars"

5. Оператор With для файлового объекта

Нам часто нужно читать данные из файлов и записывать данные в файлы. Самый распространенный способ - просто открыть файл с помощью встроенной функции open(), которая создает файловый объект, с которым мы можем работать. Сталкивались ли вы раньше со следующей проблемой?

>>> # Create a text file that has the text: Hello World!
... 
... # Open the file and append some new data
... text_file0 = open("hello_world.txt", "a")
... text_file0.write("Hello Python!")
... 
... # Open the file again for something else
... text_file1 = open("hello_world.txt")
... print(text_file1.read())
... 
Hello World!

В предыдущем фрагменте кода мы начинаем с текстового файла с текстом «Hello World!» Затем мы добавляем в файл новые данные. Однако через некоторое время мы снова захотим поработать с файлом; текстовый файл все еще имеет старые данные, когда мы его читаем. Другими словами, добавленные тексты не включаются в текстовый файл. Почему так могло случиться?

Это потому, что мы изначально не закрыли файловый объект. Без закрытия файла изменения не могут быть сохранены. Действительно, мы можем явно вызвать метод close() для файлового объекта. Однако мы можем сделать это с помощью оператора «with», который автоматически закроет для нас файловый объект, как показано ниже. Когда мы закончили операцию с файлом, мы можем убедиться, что файл закрыт, обратившись к атрибуту closed файлового объекта.

>>> with open("hello_world.txt", "a") as file:
...     file.write("Hello Python!")
... 
... with open("hello_world.txt") as file:
...     print(file.read())
... 
... print("Is file close?", file.closed)
... 
Hello World!Hello Python!Hello Python!
Is file close? True

В более общем смысле оператор with - это синтаксис для использования диспетчеров контекста в Python. Предыдущий пример включает операцию с файлом, потому что файлы являются общими ресурсами, и мы несем ответственность за освобождение этих ресурсов. Контекстные менеджеры могут помочь нам выполнить работу. Как было показано ранее, после завершения операции с файлом файл автоматически закрывается с помощью оператора with. Подробнее об управлении контекстом вы можете узнать в моей предыдущей статье.

6. Оцените несколько условий

Часто нам нужно оценить несколько условий. Есть несколько возможных сценариев. Для числовых значений у нас может быть несколько сравнений для одной и той же переменной. В этом случае мы можем связать эти сравнения.

# Multiple Comparisons
# The typical way
if a < 4 and a > 1:
    # do something here
# Do this instead
if 1 < a < 4:
    # do somerthing here

В некоторых других сценариях у нас может быть несколько сравнений на равенство, и мы можем воспользоваться следующим методом, используя ключевое слово in для тестирования членства.

# The typical way
if b == "Mon" or b == "Wed" or b == "Fri" or b == "Sun":
    # do something here
# Do this instead, you can also specify a tuple ("Mon", "Wed", "Fri", "Sun")
if b in "Mon Wed Fri Sun".split():
    # do something here

Другой метод - использование встроенных функций all() и any() для оценки нескольких условий. В частности, функция all() будет оцениваться как True, когда все элементы в итерируемом элементе True, и, таким образом, эта функция подходит для замены серии логических сравнений AND. С другой стороны, функция any() будет оцениваться как True, когда любой элемент в итерируемом элементе равен True, и, таким образом, ее можно заменить серией логических операций ИЛИ. Соответствующие примеры показаны ниже.

# The typical ways
if a < 10 and b > 5 and c == 4:
    # do something
if a < 10 or b > 5 or c == 4:
    # do something
# Do these instead
if all([a < 10, b > 5, c == 4]):
    # do something
if any([a < 10, b > 5, c == 4]):
    # do something

7. Используйте значения по умолчанию в объявлениях функций

Почти во всех проектах Python большая часть кода включает создание и вызов функций. Другими словами, мы постоянно имеем дело с объявлениями функций и рефакторингом. Во многих сценариях нам нужно вызывать функцию несколько раз. В зависимости от различных наборов параметров функция будет работать по-разному. Однако иногда один набор параметров может использоваться чаще, чем другие, и в этом случае мы должны рассмотреть возможность установки значений по умолчанию при объявлении функций. Рассмотрим следующий тривиальный пример.

# The original form:
def generate_plot(data, image_name):
    """This function creates a scatter plot for the data"""
    # create the plot based on the data
    ...
    if image_name:
        # save the image
        ...
# In many cases, we don't need to save the image
generate_plot(data, None)
# The one with a default value
def generate_plot(data, image_name=None):
    pass
# Now, we can omit the second parameter
generate_plot(data)

Следует отметить, что если вы имеете дело с изменяемыми типами данных (например, списками, наборами) при установке значения по умолчанию, убедитесь, что вы используете None вместо конструктора (например, arg_name = []). Поскольку Python создает объект функции там, где он определен, предоставленный пустой список застрянет с объектом функции. Другими словами, функциональный объект не будет создан "на лету", когда вы его вызываете. Вместо этого вы будете иметь дело с одним и тем же функциональным объектом, включая его изначально созданный изменяемый объект по умолчанию, в памяти, что может привести к неожиданному поведению (см. Здесь для более подробного обсуждения).

8. Используйте счетчик для подсчета элементов.

Когда у нас есть несколько элементов в списке, кортеже или строке (например, несколько символов), мы часто хотим подсчитать, сколько их имеется для каждого элемента. Для этого можно написать утомительный код для этой функции.

>>> words = ['an', 'boy', 'girl', 'an', 'boy', 'dog', 'cat', 'Dog', 'CAT', 'an','GIRL', 'AN', 'dog', 'cat', 'cat', 'bag', 'BAG', 'BOY', 'boy', 'an']
... unique_words = {x.lower() for x in set(words)}
... for word in unique_words:
...     print(f"* Count of {word}: {words.count(word)}")
... 
* Count of cat: 3
* Count of bag: 1
* Count of boy: 3
* Count of dog: 2
* Count of an: 5
* Count of girl: 1

Как показано выше, сначала нам нужно было создать набор, включающий только уникальные слова. Затем мы повторили набор слов и использовали метод count(), чтобы узнать, где встречается каждое слово. Однако есть лучший способ сделать это - использовать класс Counter, который предназначен для выполнения этой задачи подсчета.

>>> from collections import Counter
... 
... word_counter = Counter(x.lower() for x in words)
... print("Word Counts:", word_counter)
... 
Word Counts: Counter({'an': 5, 'boy': 4, 'cat': 4, 'dog': 3, 'girl': 2, 'bag': 2})

Класс Counter доступен в модуле collections. Чтобы использовать класс, мы просто создали генератор: x.lower() for x in words, и каждый элемент будет подсчитан. Как видите, объект Counter представляет собой объект сопоставления, подобный dict, в котором каждый ключ соответствует уникальному элементу списка слов, а значения являются счетчиками для этих элементов. Довольно лаконично, правда?

Более того, если вы хотите узнать наиболее часто встречающиеся элементы списка слов, мы можем воспользоваться методом most_common() объекта Counter. Следующий код показывает вам это использование. Вам просто нужно указать целое число (N), которое определит наиболее часто встречающиеся N элементов из списка. В качестве примечания, объект Counter также будет работать с другими данными последовательности, такими как строки и кортежи.

>>> # Find out the most common item
... print("Most Frequent:", word_counter.most_common(1))
Most Frequent: [('an', 5)]
>>> # Find out the most common 2 items
... print("Most Frequent:", word_counter.most_common(2))
Most Frequent: [('an', 5), ('boy', 4)]

9. Сортировка с различными требованиями к порядку.

Сортировка элементов в списке - распространенная задача во многих проектах. Самая простая сортировка основана на числовом или алфавитном порядке, и мы можем использовать встроенную функцию sorted(). По умолчанию функция sorted() сортирует список (фактически, он может быть любым итеративным) в порядке возрастания. Если мы укажем аргумент reverse равным True, мы сможем получить элементы в порядке убывания. Ниже показаны некоторые простые способы использования.

>>> # A list of numbers and strings
... numbers = [1, 3, 7, 2, 5, 4]
... words = ['yay', 'bill', 'zen', 'del']
... # Sort them
... print(sorted(numbers))
... print(sorted(words))
... 
[1, 2, 3, 4, 5, 7]
['bill', 'del', 'yay', 'zen']
>>> # Sort them in descending order
... print(sorted(numbers, reverse=True))
... print(sorted(words, reverse=True))
... 
[7, 5, 4, 3, 2, 1]
['zen', 'yay', 'del', 'bill']

Помимо этих основных способов использования, мы можем указать аргумент key, чтобы можно было сортировать сложные элементы, такие как список кортежей. Рассмотрим следующий пример для такого случая.

>>> # Create a list of tuples
... grades = [('John', 95), ('Aaron', 99), ('Zack', 97), ('Don', 92), ('Jennifer', 100), ('Abby', 94), ('Zoe', 99), ('Dee', 93)]
>>> # Sort by the grades, descending
... sorted(grades, key=lambda x: x[1], reverse=True)
[('Jennifer', 100), ('Aaron', 99), ('Zoe', 99), ('Zack', 97), ('John', 95), ('Abby', 94), ('Dee', 93), ('Don', 92)]
>>> # Sort by the name's initial letter, ascending
... sorted(grades, key=lambda x: x[0][0])
[('Aaron', 99), ('Abby', 94), ('Don', 92), ('Dee', 93), ('John', 95), ('Jennifer', 100), ('Zack', 97), ('Zoe', 99)]

В приведенном выше коде показаны два расширенных примера сортировки с использованием лямбда-функции, которая передается в аргумент key. Первый сортирует элементы в порядке убывания, а второй - в порядке возрастания по умолчанию. Что, если мы хотим совместить эти два требования? Если вы подумаете об игре с аргументом reverse, вы, вероятно, откроете не то дерево, потому что, если вы пытаетесь сортировать по нескольким критериям, обратный аргумент будет применяться ко всем. В чем же тогда фокус? См. Фрагмент кода ниже.

>>> # Requirement: sort by name initial ascending, and by grades, descending
... # Both won't work
... sorted(grades, key=lambda x: (x[0][0], x[1]), reverse=True)
[('Zoe', 99), ('Zack', 97), ('Jennifer', 100), ('John', 95), ('Dee', 93), ('Don', 92), ('Aaron', 99), ('Abby', 94)]
>>> sorted(grades, key=lambda x: (x[0][0], x[1]), reverse=False)
[('Abby', 94), ('Aaron', 99), ('Don', 92), ('Dee', 93), ('John', 95), ('Jennifer', 100), ('Zack', 97), ('Zoe', 99)]
>>> # This will do the trick
... sorted(grades, key=lambda x: (x[0][0], -x[1]))
[('Aaron', 99), ('Abby', 94), ('Dee', 93), ('Don', 92), ('Jennifer', 100), ('John', 95), ('Zoe', 99), ('Zack', 97)]

Как видите, установив для аргумента reverse значение True или False, ни один из них не работал. Вместо этого хитрость состоит в том, чтобы отрицать оценки, и, таким образом, когда вы выполняете сортировку в порядке возрастания по умолчанию, оценки будут отсортированы в обратном порядке из-за отрицания этих значений. Однако для этого метода есть предостережение, потому что отрицание может работать только с числовыми значениями, но не со строками.

10. Не забывайте defaultdict

Словари - это мощный тип данных, который позволяет нам хранить данные в виде пар ключ-значение. Требуется, чтобы все ключи были хешируемыми, чтобы при хранении этих данных можно было использовать хеш-таблицу. Такая реализация обеспечивает эффективность извлечения и вставки данных O (1). Однако следует отметить, что помимо встроенного типа dict у нас есть альтернативные словари, которые мы можем использовать. Среди них я хотел бы обсудить тип defaultdict. В отличие от встроенного типа dict, defaultdict позволяет нам установить заводскую функцию по умолчанию, которая создает элемент, когда ключ не существует. Вы, вероятно, знакомы со следующей ошибкой.

>>> student = {'name': "John", 'age': 18}
... student['gender']
... 
Traceback (most recent call last):
  File "<input>", line 2, in <module>
KeyError: 'gender'

Предположим, что мы имеем дело со словами и хотим сгруппировать те же символы, что и список, и эти списки связаны с символами, являющимися ключами. Вот наивная реализация с использованием встроенного типа dict. В частности, очень важно проверить, имеет ли объект dict ключ letter, потому что вызов метода append() может вызвать KeyError исключение, если ключ не существует.

>>> letters = ["a", "a", "c", "d", "d", "c", "a", "b"]
... final_dict = {}
... for letter in letters:
...     if letter not in final_dict:
...         final_dict[letter] = []
...     final_dict[letter].append(letter)
... 
... print("Final Dict:", final_dict)
... 
Final Dict: {'a': ['a', 'a', 'a'], 'c': ['c', 'c'], 'd': ['d', 'd'], 'b': ['b']}

Давайте посмотрим, как мы можем использовать defaultdict для написания более лаконичного кода. Хотя пример тривиален, он просто дает вам некоторые идеи о классе defaultdict, который избавляет нас от необходимости иметь дело с несуществующими ключами среди объектов словаря.

>>> from collections import defaultdict
... 
... final_defaultdict = defaultdict(list)
... for letter in letters:
...     final_defaultdict[letter].append(letter)
... 
... print("Final Default Dict:", final_defaultdict)
... 
Final Default Dict: defaultdict(<class 'list'>, {'a': ['a', 'a', 'a'], 'c': ['c', 'c'], 'd': ['d', 'd'], 'b': ['b']})

Выводы

Возможно, вы познакомитесь с некоторыми из этих приемов до того, как прочтете эту статью, но я надеюсь, что вы все еще хорошо освежили эти навыки. Практика этих идиоматических обычаев в ваших проектах сделает ваш код Python более читабельным и производительным.

Вот и все. Спасибо за прочтение.