Визначення динамічного класу в Python

Ось акуратний трюк на Python, який одного дня може вам стати корисним. Давайте розглянемо, як ви можете динамічно визначати класи та створювати їх екземпляри за необхідності.

Цей фокус використовує можливості об’єктно-орієнтованого програмування (OOP) Python, тому ми розглянемо їх спочатку.

Класи та предмети

Python - це об’єктно-орієнтована мова, тобто вона дозволяє писати код в об’єктно-орієнтованій парадигмі.

Ключовою концепцією цієї парадигми програмування є класи. У Python вони використовуються для створення об'єктів, які можуть мати атрибути.

Об'єкти - це конкретні екземпляри класу. Клас - це, по суті, план того, що таке об’єкт і як він повинен поводитися.

Класи визначаються двома типами атрибутів:

  • Атрибути даних - змінні, доступні для даного екземпляра цього класу
  • Методи - функції, доступні для екземпляра цього класу

Класичний приклад ООП зазвичай включає різні типи тварин або їжу. Тут я більш практичний із простою темою візуалізації даних.

Спочатку визначте клас BarChart.

class BarChart: def __init__(self, title, data): self.title = title self.data = data def plot(self): print("\n"+self.title) for k in self.data.keys(): print("-"*self.data[k]+" "+k)

__init__Метод дозволяє встановити атрибути інстанцірованія. Тобто, коли ви створюєте новий екземпляр BarChart, ви можете передавати аргументи, що забезпечують заголовок та дані діаграми.

Цей клас також має plot()метод. Це друкує дуже базову гістограму на консолі під час її виклику. Це могло б зробити більше цікавих речей у реальному додатку.

Далі створіть екземпляр екземпляра BarChart:

data = {"a":4, "b":7, "c":8}bar = BarChart("A Simple Chart", data)

Тепер ви можете використовувати barоб'єкт у решті коду:

bar.data['d'] = bar.plot()
A Simple Chart ---- a ------- b -------- c ----- d

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

new_data = {"x":1, "y":2, "z":3} bar2 = BarChart("Another Chart", new_data) bar2.plot()
Another Chart - x -- y --- z

Скажімо, ви хотіли визначити кілька класів діаграми. Спадкування дозволяє визначити класи, які “успадковують” властивості від базових класів.

Наприклад, ви можете визначити базовий Chartклас. Тоді ви можете визначити похідні класи, які успадковуються від бази.

class Chart: def __init__(self, title, data): self.title = title self.data = data def plot(self): pass
class BarChart(Chart): def plot(self): print("\n"+self.title) for k in self.data.keys(): print("-"*self.data[k]+" "+k)
class Scatter(Chart): def plot(self): points = zip(data['x'],data['y']) y = max(self.data['y'])+1 x = max(self.data['x'])+1 print("\n"+self.title) for i in range(y,-1,-1): line = str(i)+"|" for j in range(x): if (j,i) in points: line += "X" else: line += " " print(line)

Тут Chartклас є базовим класом. BarChartІ Scatterкласи успадковують __init__()метод від Chart.Але у них є свої власні plot()методи , які заміщають один певний в Chart.

Тепер ви також можете створювати об'єкти діаграми розсіювання.

data = {'x':[1,2,4,5], 'y':[1,2,3,4]} scatter = Scatter('Scatter Chart', data) scatter.plot()
Scatter Chart 4| X 3| X 2| X 1| X 0|

Цей підхід дозволяє писати більш абстрактний код, надаючи додатку більшу гнучкість. Наявність креслень для створення незліченних варіацій одного і того ж загального об’єкта заощадить вам зайво повторювані рядки коду. Це також може полегшити розуміння коду вашої програми.

Ви також можете імпортувати класи в майбутні проекти, якщо ви хочете використати їх пізніше.

Заводські методи

Іноді ви не будете знати конкретний клас, який ви хочете реалізувати до виконання. Наприклад, можливо, створені вами об'єкти залежатимуть від вводу користувача або результатів іншого процесу зі змінним результатом.

Заводські методи пропонують рішення. Це методи, які беруть динамічний список аргументів і повертають об'єкт. Надані аргументи визначають клас об’єкта, який повертається.

Простий приклад наведено нижче. Ця фабрика може повернути або гістограму, або об'єкт розсіювання, залежно від styleаргументу, який вона отримує. Більш розумний фабричний метод міг би навіть здогадатися, який найкращий клас використовувати, розглядаючи структуру dataаргументу.

def chart_factory(title, data, style): if style == "bar": return BarChart(title, data) if style == "scatter": return Scatter(title, data) else: raise Exception("Unrecognized chart style.") 
chart = chart_factory("New Chart", data, "bar") chart.plot()

Заводські методи є чудовими, коли ти заздалегідь знаєш, які класи хочеш повернути, та умови, за яких вони повертаються.

Але що, якщо ти навіть не знаєш цього заздалегідь?

Динамічні визначення

Python дозволяє динамічно визначати класи та створювати з ними об'єкти за необхідності.

Чому ви можете зробити це? Коротка відповідь - це ще абстракція.

Слід визнати, що необхідність писати код на цьому рівні абстракції, як правило, рідкість. Як завжди при програмуванні, вам слід подумати, чи є простіше рішення.

Однак бувають випадки, коли справді виявляється корисним динамічне визначення класів. Нижче ми розглянемо можливий варіант використання.

Можливо, ви знайомі з type()функцією Python . За допомогою одного аргументу він просто повертає „тип” об’єкта аргументу.

type(1) #  type('hello') #  type(True) # 

Але, з трьома аргументами, type()повертає абсолютно новий об’єкт типу. Це еквівалентно визначенню нового класу.

NewClass = type('NewClass', (object,), {})
  • Перший аргумент - це рядок, який дає новому класу назву
  • Наступним є кортеж, який містить будь-які базові класи, від яких новий клас повинен успадкувати
  • Остаточний аргумент - це словник атрибутів, характерних для цього класу

Коли може знадобитися використовувати щось таке абстрактне, як це? Розглянемо наступний приклад.

Flask Table - це бібліотека Python, яка генерує синтаксис для таблиць HTML. Його можна встановити через менеджер пакунків pip.

You can use Flask Table to define classes for each table you want to generate. You define a class that inherits from a base Table class. Its attributes are column objects, which are instances of the Col class.

from flask_table import Table, Col class MonthlyDownloads(Table): month = Col('Month') downloads = Col('Downloads') data = [{'month':'Jun', 'downloads':700}, {'month':'Jul', 'downloads':900}, {'month':'Aug', 'downloads':1600}, {'month':'Sep', 'downloads':1900}, {'month':'Oct', 'downloads':2200}] table = MonthlyDownloads(data)print(table.__html__())

You then create an instance of the class, passing in the data you want to display. The __html__() method generates the required HTML.

Now, say you’re developing a tool that uses Flask Table to generate HTML tables based on a user-provided config file. You don’t know in advance how many columns the user wants to define — it could be one, it could be a hundred! How can your code define the right class for the job?

Dynamic class definition is useful here. For each class you wish to define, you can dynamically build the attributes dictionary.

Say your user config is a CSV file, with the following structure:

Table1, column1, column2, column3 Table2, column1 Table3, column1, column2

You could read the CSV file line-by-line, using the first element of each row as the name of each table class. The remaining elements in that row would be used to define Col objects for that table class. These are added to an attributes dictionary, which is built up iteratively.

for row in csv_file: attributes = {} for column in row[1:]: attributes[column] = Col(column) globals()[row[0]] = type(row[0], (Table,), attributes)

The code above defines classes for each of the tables in the CSV config file. Each class is added to the globals dictionary.

Of course, this is a relatively trivial example. FlaskTable is capable of generating much more sophisticated tables. A real life use-case would make better use of this! But, hopefully, you’ve seen how dynamic class definition might prove useful in some contexts.

So now you know…

If you are new to Python, then it is worth getting up to speed with classes and objects early on. Try implementing them in your next learning project. Or, browse open source projects on Github to see how other developers make use of them.

For those with a little more experience, it can be very rewarding to learn how things work “behind-the-scenes”. Browsing the official docs can be illuminating!

Have you ever found a use-case for dynamic class definition in Python? If so, it’d be great to share it in the responses below.