Соглашения по передаче аргументов функций

Beginner

This tutorial is from open-source community. Access the source code

Введение

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

Цель этого упражнения - переписать файл stock.py более организованным образом. Перед началом скопируйте существующую работу из файла stock.py в новый файл с именем orig_stock.py для справки. Файлы, которые вы создадите, - это structure.py и stock.py.

Понимание передачи аргументов функций

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

Создание резервной копии вашей работы

Перед тем как мы начнем вносить изменения в файл stock.py, хорошей практикой является создание резервной копии. Таким образом, если что-то пойдет не так во время наших экспериментов, мы всегда сможем вернуться к исходной версии. Чтобы создать резервную копию, откройте терминал и выполните следующую команду:

cp stock.py orig_stock.py

Эта команда использует команду cp (копирование) в терминале. Она берет файл stock.py и создает его копию с именем orig_stock.py. Таким образом, мы обеспечиваем сохранность нашей исходной работы.

Исследование передачи аргументов функций

В Python существует несколько способов вызова функций с разными типами аргументов. Давайте подробно рассмотрим каждый из этих методов.

1. Позиционные аргументы

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

Вот пример:

def calculate(x, y, z):
    return x + y + z

## Call with positional arguments
result = calculate(1, 2, 3)
print(result)  ## Output: 6

В этом примере функция calculate принимает три параметра: x, y и z. Когда мы вызываем функцию с calculate(1, 2, 3), значение 1 присваивается x, 2 присваивается y, а 3 присваивается z. Затем функция складывает эти значения и возвращает результат.

2. Ключевые аргументы

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

Вот пример:

## Call with a mix of positional and keyword arguments
result = calculate(1, z=3, y=2)
print(result)  ## Output: 6

В этом примере мы сначала передаем позиционный аргумент 1 для x. Затем мы используем ключевые аргументы, чтобы указать значения для y и z. Порядок ключевых аргументов не имеет значения, главное, чтобы вы указали правильные имена.

3. Распаковка последовательностей и словарей

Python предоставляет удобный способ передачи последовательностей и словарей в качестве аргументов с использованием синтаксиса * и **. Это называется распаковкой.

Вот пример распаковки кортежа в позиционные аргументы:

## Unpacking a tuple into positional arguments
args = (1, 2, 3)
result = calculate(*args)
print(result)  ## Output: 6

В этом примере у нас есть кортеж args, который содержит значения 1, 2 и 3. Когда мы используем оператор * перед args при вызове функции, Python распаковывает кортеж и передает его элементы в качестве позиционных аргументов функции calculate.

Вот пример распаковки словаря в ключевые аргументы:

## Unpacking a dictionary into keyword arguments
kwargs = {'y': 2, 'z': 3}
result = calculate(1, **kwargs)
print(result)  ## Output: 6

В этом примере у нас есть словарь kwargs, который содержит пары ключ-значение 'y': 2 и 'z': 3. Когда мы используем оператор ** перед kwargs при вызове функции, Python распаковывает словарь и передает его пары ключ-значение в качестве ключевых аргументов функции calculate.

4. Прием переменного количества аргументов

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

Вот пример функции, которая принимает любое количество позиционных аргументов:

## Accept any number of positional arguments
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2))           ## Output: 3
print(sum_all(1, 2, 3, 4, 5))  ## Output: 15

В этом примере функция sum_all использует параметр *args для приема любого количества позиционных аргументов. Оператор * собирает все позиционные аргументы в кортеж с именем args. Затем функция использует встроенную функцию sum для сложения всех элементов кортежа.

Вот пример функции, которая принимает любое количество ключевых аргументов:

## Accept any number of keyword arguments
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Python", year=1991)
## Output:
## name: Python
## year: 1991

В этом примере функция print_info использует параметр **kwargs для приема любого количества ключевых аргументов. Оператор ** собирает все ключевые аргументы в словарь с именем kwargs. Затем функция проходит по парам ключ-значение в словаре и выводит их.

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

python3

Как только вы войдете в интерпретатор Python, попробуйте ввести приведенные выше примеры. Это даст вам практический опыт работы с этими техниками передачи аргументов.

Создание базового класса структуры

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

Проблема с повторяющимся кодом

В предыдущих упражнениях вы определили класс Stock, как показано ниже:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

Создание гибкого базового класса

Давайте создадим базовый класс Structure, который может автоматически обрабатывать назначение атрибутов. Сначала откройте WebIDE и создайте новый файл с именем structure.py. Затем добавьте следующий код в этот файл:

## structure.py

class Structure:
    """
    A base class for creating simple data structures.
    Automatically populates object attributes from _fields and constructor arguments.
    """
    _fields = ()

    def __init__(self, *args):
        ## Check that the number of arguments matches the number of fields
        if len(args) != len(self._fields):
            raise TypeError(f"Expected {len(self._fields)} arguments")

        ## Set the attributes
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

Этот базовый класс имеет несколько важных особенностей:

  1. Он определяет переменную класса _fields. По умолчанию эта переменная пуста. Эта переменная будет хранить имена атрибутов, которые будет иметь класс.
  2. Он проверяет, совпадает ли количество аргументов, переданных в конструктор, с количеством полей, определенных в _fields. Если они не совпадают, он вызывает исключение TypeError. Это помогает нам выявлять ошибки на ранней стадии.
  3. Он устанавливает атрибуты объекта, используя имена полей и значения, предоставленные в качестве аргументов. Функция setattr используется для динамического установки атрибутов.

Тестирование нашего базового класса структуры

Теперь давайте создадим несколько примеров классов, которые наследуются от базового класса Structure. Добавьте следующий код в файл structure.py:

## Example classes using Structure
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

class Point(Structure):
    _fields = ('x', 'y')

class Date(Structure):
    _fields = ('year', 'month', 'day')

Чтобы проверить, работает ли наша реализация правильно, мы создадим тестовый файл с именем test_structure.py. Добавьте следующий код в этот файл:

## test_structure.py
from structure import Stock, Point, Date

## Test Stock class
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}, shares: {s.shares}, price: {s.price}")

## Test Point class
p = Point(3, 4)
print(f"Point coordinates: ({p.x}, {p.y})")

## Test Date class
d = Date(2023, 11, 9)
print(f"Date: {d.year}-{d.month}-{d.day}")

## Test error handling
try:
    s2 = Stock('AAPL', 50)  ## Missing price argument
    print("This should not print")
except TypeError as e:
    print(f"Error correctly caught: {e}")

Чтобы запустить тест, откройте терминал и выполните следующую команду:

python3 test_structure.py

Вы должны увидеть следующий вывод:

Stock name: GOOG, shares: 100, price: 490.1
Point coordinates: (3, 4)
Date: 2023-11-9
Error correctly caught: Expected 3 arguments

Как вы можете видеть, наш базовый класс работает как ожидалось. Он значительно упростил определение новых структур данных без необходимости повторно писать один и тот же шаблонный код.

Улучшение представления объектов

Наш класс Structure полезен для создания и доступа к объектам. Однако в настоящее время у него нет хорошего способа представить себя в виде строки. Когда вы выводите объект или просматриваете его в интерпретаторе Python, вы хотите видеть четкое и информативное отображение. Это помогает вам понять, что представляет собой объект и какие у него значения.

Понимание представления объектов в Python

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

  • __str__ - Этот метод используется функцией str() и функцией print(). Он предоставляет человекочитаемое представление объекта. Например, если у вас есть объект Stock, метод __str__ может вернуть что - то вроде "Stock: GOOG, 100 shares at $490.1".
  • __repr__ - Этот метод используется интерпретатором Python и функцией repr(). Он дает более техническое и однозначное представление объекта. Цель метода __repr__ - предоставить строку, которая может быть использована для воссоздания объекта. Например, для объекта Stock он может вернуть "Stock('GOOG', 100, 490.1)".

Давайте добавим метод __repr__ в наш класс Structure. Это облегчит отладку нашего кода, так как мы сможем четко видеть состояние наших объектов.

Реализация хорошего представления

Теперь вам нужно обновить файл structure.py. Вы добавите метод __repr__ в класс Structure. Этот метод создаст строку, которая представляет объект таким образом, что можно использовать для его воссоздания.

def __repr__(self):
    """
    Return a representation of the object that can be used to recreate it.
    Example: Stock('GOOG', 100, 490.1)
    """
    ## Get the class name
    cls_name = type(self).__name__

    ## Get all the field values
    values = [getattr(self, name) for name in self._fields]

    ## Format the fields and values
    args_str = ', '.join(repr(value) for value in values)

    ## Return the formatted string
    return f"{cls_name}({args_str})"

Вот что делает этот метод пошагово:

  1. Он получает имя класса с помощью type(self).__name__. Это важно, так как оно говорит, с каким типом объекта вы имеете дело.
  2. Он извлекает все значения полей из экземпляра. Это дает вам данные, которые хранит объект.
  3. Он создает строковое представление с именем класса и значениями. Эта строка может быть использована для воссоздания объекта.

Тестирование улучшенного представления

Давайте протестируем наше улучшенное решение. Создайте новый файл с именем test_repr.py. Этот файл создаст несколько экземпляров наших классов и выведет их представления.

## test_repr.py
from structure import Stock, Point, Date

## Create instances
s = Stock('GOOG', 100, 490.1)
p = Point(3, 4)
d = Date(2023, 11, 9)

## Print the representations
print(repr(s))
print(repr(p))
print(repr(d))

## Direct printing also uses __repr__ in the interpreter
print(s)
print(p)
print(d)

Чтобы запустить тест, откройте терминал и введите следующую команду:

python3 test_repr.py

Вы должны увидеть следующий вывод:

Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)
Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)

Этот вывод намного более информативен, чем раньше. Когда вы видите Stock('GOOG', 100, 490.1), вы сразу понимаете, что представляет собой объект. Вы даже можете скопировать эту строку и использовать ее для воссоздания объекта в своем коде.

Преимущества хорошего представления

Хорошая реализация метода __repr__ очень полезна для отладки. Когда вы просматриваете объекты в интерпретаторе или логируете их во время выполнения программы, четкое представление позволяет быстро выявить проблемы. Вы можете увидеть точное состояние объекта и понять, что может быть не так.

Ограничение имен атрибутов

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

Необходимость ограничения атрибутов

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

s = Stock('GOOG', 100, 490.1)
s.shares = 50      ## Correct attribute name
s.share = 60       ## Typo in attribute name - creates a new attribute instead of updating

Во второй строке допущена опечатка. Вместо shares мы написали share. В Python, вместо того чтобы вызвать ошибку, он просто создаст новый атрибут с именем share. Это может привести к неочевидным ошибкам, так как вы можете думать, что обновляете атрибут shares, но на самом деле создаете новый. Это может сделать поведение вашего кода непредсказуемым и очень сложным для отладки.

Реализация ограничения атрибутов

Чтобы решить эту проблему, мы можем переопределить метод __setattr__. Этот метод вызывается каждый раз, когда вы пытаетесь установить атрибут объекта. Переопределив его, мы можем контролировать, какие атрибуты можно устанавливать, а какие - нет.

Обновите класс Structure в файле structure.py следующим кодом:

def __setattr__(self, name, value):
    """
    Restrict attribute setting to only those defined in _fields
    or attributes starting with underscore (private attributes).
    """
    if name.startswith('_'):
        ## Allow setting private attributes (starting with '_')
        super().__setattr__(name, value)
    elif name in self._fields:
        ## Allow setting attributes defined in _fields
        super().__setattr__(name, value)
    else:
        ## Raise an error for other attributes
        raise AttributeError(f'No attribute {name}')

Вот как работает этот метод:

  1. Если имя атрибута начинается с подчеркивания (_), он считается приватным атрибутом. Приватные атрибуты часто используются для внутренних целей в классе. Мы позволяем устанавливать эти атрибуты, так как они являются частью внутренней реализации класса.
  2. Если имя атрибута находится в списке _fields, это означает, что он является одним из атрибутов, определенных в дизайне класса. Мы позволяем устанавливать эти атрибуты, так как они являются частью ожидаемого поведения класса.
  3. Если имя атрибута не удовлетворяет ни одному из этих условий, мы вызываем исключение AttributeError. Это сообщает пользователю, что он пытается установить атрибут, который не существует в классе.

Тестирование ограничения атрибутов

Теперь, когда мы реализовали ограничение атрибутов, давайте протестируем его, чтобы убедиться, что он работает как ожидается. Создайте файл с именем test_attributes.py со следующим кодом:

## test_attributes.py
from structure import Stock

s = Stock('GOOG', 100, 490.1)

## This should work - valid attribute
print("Setting shares to 50")
s.shares = 50
print(f"Shares is now: {s.shares}")

## This should work - private attribute
print("\nSetting _internal_data")
s._internal_data = "Some data"
print(f"_internal_data is: {s._internal_data}")

## This should fail - invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.share = 60  ## Typo in attribute name
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

Чтобы запустить тест, откройте терминал и введите следующую команду:

python3 test_attributes.py

Вы должны увидеть следующий вывод:

Setting shares to 50
Shares is now: 50

Setting _internal_data
_internal_data is: Some data

Trying to set an invalid attribute:
Error correctly caught: No attribute share

Этот вывод показывает, что наш класс теперь предотвращает случайные ошибки с атрибутами. Он позволяет нам устанавливать допустимые атрибуты и приватные атрибуты, но вызывает ошибку, когда мы пытаемся установить недопустимый атрибут.

Ценность ограничения атрибутов

Ограничение имен атрибутов очень важно для написания надежного и поддерживаемого кода. Вот почему:

  1. Это помогает обнаружить опечатки в именах атрибутов. Если вы допустите ошибку при вводе имени атрибута, код вызовет ошибку вместо создания нового атрибута. Это делает проще найти и исправить ошибки на ранней стадии разработки.
  2. Это предотвращает попытки установить атрибуты, которые не существуют в дизайне класса. Это гарантирует, что класс используется как задумано и что код ведет себя предсказуемо.
  3. Это избавляет от случайного создания новых атрибутов. Создание новых атрибутов может привести к непредвиденному поведению и сделать код труднее для понимания и поддержки.

Ограничивая имена атрибутов, мы делаем наш код более надежным и удобным для работы.

Переписывание класса Stock

Теперь, когда у нас есть хорошо определенный базовый класс Structure, пришло время переписать наш класс Stock. Используя этот базовый класс, мы можем упростить наш код и сделать его более организованным. Класс Structure предоставляет набор общих функций, которые мы можем повторно использовать в нашем классе Stock, что является большим преимуществом для поддерживаемости и читаемости кода.

Создание нового класса Stock

Начнем с создания нового файла с именем stock.py. Этот файл будет содержать наш переписанный класс Stock. Вот код, который нужно поместить в файл stock.py:

## stock.py
from structure import Structure

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

    @property
    def cost(self):
        """
        Calculate the cost as shares * price
        """
        return self.shares * self.price

    def sell(self, nshares):
        """
        Sell a number of shares
        """
        self.shares -= nshares

Разберем, что делает этот новый класс Stock:

  1. Он наследуется от класса Structure. Это означает, что класс Stock может использовать все функции, предоставляемые классом Structure. Одно из преимуществ заключается в том, что нам не нужно самостоятельно писать метод __init__, так как класс Structure автоматически обрабатывает назначение атрибутов.
  2. Мы определяем _fields, который представляет собой кортеж, указывающий атрибуты класса Stock. Эти атрибуты - name, shares и price.
  3. Определено свойство cost для вычисления общей стоимости акций. Оно умножает количество shares на price.
  4. Метод sell используется для уменьшения количества акций. Когда вы вызываете этот метод с количеством акций для продажи, он вычитает это количество из текущего количества акций.

Тестирование нового класса Stock

Чтобы убедиться, что наш новый класс Stock работает как ожидается, нам нужно создать тестовый файл. Создадим файл с именем test_stock.py со следующим кодом:

## test_stock.py
from stock import Stock

## Create a stock
s = Stock('GOOG', 100, 490.1)

## Check the attributes
print(f"Stock: {s}")
print(f"Name: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost}")

## Sell some shares
print("\nSelling 20 shares...")
s.sell(20)
print(f"Shares after selling: {s.shares}")
print(f"Cost after selling: {s.cost}")

## Try to set an invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.prices = 500  ## Invalid attribute (should be 'price')
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

В этом тестовом файле мы сначала импортируем класс Stock из файла stock.py. Затем создаем экземпляр класса Stock с именем 'GOOG', 100 акциями и ценой 490.1. Мы выводим на экран атрибуты акции, чтобы проверить, правильно ли они заданы. Затем мы продаем 20 акций и выводим новое количество акций и новую стоимость. Наконец, мы пытаемся установить недопустимый атрибут prices (должно быть price). Если наш класс Stock работает правильно, он должен вызвать исключение AttributeError.

Чтобы запустить тест, откройте терминал и введите следующую команду:

python3 test_stock.py

Ожидаемый вывод выглядит следующим образом:

Stock: Stock('GOOG', 100, 490.1)
Name: GOOG
Shares: 100
Price: 490.1
Cost: 49010.0

Selling 20 shares...
Shares after selling: 80
Cost after selling: 39208.0

Trying to set an invalid attribute:
Error correctly caught: No attribute prices

Запуск модульных тестов

Если у вас есть модульные тесты из предыдущих упражнений, вы можете запустить их на новой реализации. В терминале введите следующую команду:

python3 teststock.py

Обратите внимание, что некоторые тесты могут не пройти. Это может быть связано с тем, что они ожидают определенное поведение или методы, которые мы еще не реализовали. Не волнуйтесь! Мы будем продолжать развивать этот фундамент в будущих упражнениях.

Обзор нашего прогресса

Возьмем мгновение, чтобы обзорвать, что мы достигли до сих пор:

  1. Мы создали переиспользуемый базовый класс Structure. Этот класс:

    • Автоматически обрабатывает назначение атрибутов, что экономит нас от написания большого количества повторяющегося кода.
    • Предоставляет хорошее строковое представление, что облегчает вывод и отладку наших объектов.
    • Ограничивает имена атрибутов, чтобы предотвратить ошибки, что делает наш код более надежным.
  2. Мы переписали наш класс Stock. Он:

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

Этот подход имеет несколько преимуществ для нашего кода:

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

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

Резюме

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

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