9. Classes¶
Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have attributes attached to it for maintaining its state. Class instances can also have methods (defined by its class) for modifying its state.
Compared with other programming languages, Python’s class mechanism adds classes with a minimum of new syntax and semantics. It is a mixture of the class mechanisms found in C++ and Modula-3. Python classes provide all the standard features of Object Oriented Programming: the class inheritance mechanism allows multiple base classes, a derived class can override any methods of its base class or classes, and a method can call the method of a base class with the same name. Objects can contain arbitrary amounts and kinds of data. As is true for modules, classes partake of the dynamic nature of Python: they are created at runtime, and can be modified further after creation.
In C++ terminology, normally class members (including the data members) are public (except see below Private Variables ), and all member functions are virtual. As in Modula-3, there are no shorthands for referencing the object’s members from its methods: the method function is declared with an explicit first argument representing the object, which is provided implicitly by the call. As in Smalltalk, classes themselves are objects. This provides semantics for importing and renaming. Unlike C++ and Modula-3, built-in types can be used as base classes for extension by the user. Also, like in C++, most built-in operators with special syntax (arithmetic operators, subscripting etc.) can be redefined for class instances.
(Lacking universally accepted terminology to talk about classes, I will make occasional use of Smalltalk and C++ terms. I would use Modula-3 terms, since its object-oriented semantics are closer to those of Python than C++, but I expect that few readers have heard of it.)
9.1. A Word About Names and Objects¶
Objects have individuality, and multiple names (in multiple scopes) can be bound to the same object. This is known as aliasing in other languages. This is usually not appreciated on a first glance at Python, and can be safely ignored when dealing with immutable basic types (numbers, strings, tuples). However, aliasing has a possibly surprising effect on the semantics of Python code involving mutable objects such as lists, dictionaries, and most other types. This is usually used to the benefit of the program, since aliases behave like pointers in some respects. For example, passing an object is cheap since only a pointer is passed by the implementation; and if a function modifies an object passed as an argument, the caller will see the change — this eliminates the need for two different argument passing mechanisms as in Pascal.
9.2. Python Scopes and Namespaces¶
Before introducing classes, I first have to tell you something about Python’s scope rules. Class definitions play some neat tricks with namespaces, and you need to know how scopes and namespaces work to fully understand what’s going on. Incidentally, knowledge about this subject is useful for any advanced Python programmer.
Let’s begin with some definitions.
A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future. Examples of namespaces are: the set of built-in names (containing functions such as abs() , and built-in exception names); the global names in a module; and the local names in a function invocation. In a sense the set of attributes of an object also form a namespace. The important thing to know about namespaces is that there is absolutely no relation between names in different namespaces; for instance, two different modules may both define a function maximize without confusion — users of the modules must prefix it with the module name.
By the way, I use the word attribute for any name following a dot — for example, in the expression z.real , real is an attribute of the object z . Strictly speaking, references to names in modules are attribute references: in the expression modname.funcname , modname is a module object and funcname is an attribute of it. In this case there happens to be a straightforward mapping between the module’s attributes and the global names defined in the module: they share the same namespace! 1
Attributes may be read-only or writable. In the latter case, assignment to attributes is possible. Module attributes are writable: you can write modname.the_answer = 42 . Writable attributes may also be deleted with the del statement. For example, del modname.the_answer will remove the attribute the_answer from the object named by modname .
Namespaces are created at different moments and have different lifetimes. The namespace containing the built-in names is created when the Python interpreter starts up, and is never deleted. The global namespace for a module is created when the module definition is read in; normally, module namespaces also last until the interpreter quits. The statements executed by the top-level invocation of the interpreter, either read from a script file or interactively, are considered part of a module called __main__ , so they have their own global namespace. (The built-in names actually also live in a module; this is called builtins .)
The local namespace for a function is created when the function is called, and deleted when the function returns or raises an exception that is not handled within the function. (Actually, forgetting would be a better way to describe what actually happens.) Of course, recursive invocations each have their own local namespace.
A scope is a textual region of a Python program where a namespace is directly accessible. “Directly accessible” here means that an unqualified reference to a name attempts to find the name in the namespace.
Although scopes are determined statically, they are used dynamically. At any time during execution, there are 3 or 4 nested scopes whose namespaces are directly accessible:
the innermost scope, which is searched first, contains the local names
the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contain non-local, but also non-global names
the next-to-last scope contains the current module’s global names
the outermost scope (searched last) is the namespace containing built-in names
If a name is declared global, then all references and assignments go directly to the next-to-last scope containing the module’s global names. To rebind variables found outside of the innermost scope, the nonlocal statement can be used; if not declared nonlocal, those variables are read-only (an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged).
Usually, the local scope references the local names of the (textually) current function. Outside functions, the local scope references the same namespace as the global scope: the module’s namespace. Class definitions place yet another namespace in the local scope.
It is important to realize that scopes are determined textually: the global scope of a function defined in a module is that module’s namespace, no matter from where or by what alias the function is called. On the other hand, the actual search for names is done dynamically, at run time — however, the language definition is evolving towards static name resolution, at “compile” time, so don’t rely on dynamic name resolution! (In fact, local variables are already determined statically.)
A special quirk of Python is that – if no global or nonlocal statement is in effect – assignments to names always go into the innermost scope. Assignments do not copy data — they just bind names to objects. The same is true for deletions: the statement del x removes the binding of x from the namespace referenced by the local scope. In fact, all operations that introduce new names use the local scope: in particular, import statements and function definitions bind the module or function name in the local scope.
The global statement can be used to indicate that particular variables live in the global scope and should be rebound there; the nonlocal statement indicates that particular variables live in an enclosing scope and should be rebound there.
9.2.1. Scopes and Namespaces Example¶
This is an example demonstrating how to reference the different scopes and namespaces, and how global and nonlocal affect variable binding:
The output of the example code is:
Note how the local assignment (which is default) didn’t change scope_test‘s binding of spam. The nonlocal assignment changed scope_test‘s binding of spam, and the global assignment changed the module-level binding.
You can also see that there was no previous binding for spam before the global assignment.
9.3. A First Look at Classes¶
Classes introduce a little bit of new syntax, three new object types, and some new semantics.
9.3.1. Class Definition Syntax¶
The simplest form of class definition looks like this:
Class definitions, like function definitions ( def statements) must be executed before they have any effect. (You could conceivably place a class definition in a branch of an if statement, or inside a function.)
In practice, the statements inside a class definition will usually be function definitions, but other statements are allowed, and sometimes useful — we’ll come back to this later. The function definitions inside a class normally have a peculiar form of argument list, dictated by the calling conventions for methods — again, this is explained later.
When a class definition is entered, a new namespace is created, and used as the local scope — thus, all assignments to local variables go into this new namespace. In particular, function definitions bind the name of the new function here.
When a class definition is left normally (via the end), a class object is created. This is basically a wrapper around the contents of the namespace created by the class definition; we’ll learn more about class objects in the next section. The original local scope (the one in effect just before the class definition was entered) is reinstated, and the class object is bound here to the class name given in the class definition header ( ClassName in the example).
9.3.2. Class Objects¶
Class objects support two kinds of operations: attribute references and instantiation.
Attribute references use the standard syntax used for all attribute references in Python: obj.name . Valid attribute names are all the names that were in the class’s namespace when the class object was created. So, if the class definition looked like this:
then MyClass.i and MyClass.f are valid attribute references, returning an integer and a function object, respectively. Class attributes can also be assigned to, so you can change the value of MyClass.i by assignment. __doc__ is also a valid attribute, returning the docstring belonging to the class: "A simple example class" .
Class instantiation uses function notation. Just pretend that the class object is a parameterless function that returns a new instance of the class. For example (assuming the above class):
creates a new instance of the class and assigns this object to the local variable x .
The instantiation operation (“calling” a class object) creates an empty object. Many classes like to create objects with instances customized to a specific initial state. Therefore a class may define a special method named __init__() , like this:
When a class defines an __init__() method, class instantiation automatically invokes __init__() for the newly created class instance. So in this example, a new, initialized instance can be obtained by:
Of course, the __init__() method may have arguments for greater flexibility. In that case, arguments given to the class instantiation operator are passed on to __init__() . For example,
9.3.3. Instance Objects¶
Now what can we do with instance objects? The only operations understood by instance objects are attribute references. There are two kinds of valid attribute names: data attributes and methods.
data attributes correspond to “instance variables” in Smalltalk, and to “data members” in C++. Data attributes need not be declared; like local variables, they spring into existence when they are first assigned to. For example, if x is the instance of MyClass created above, the following piece of code will print the value 16 , without leaving a trace:
The other kind of instance attribute reference is a method. A method is a function that “belongs to” an object. (In Python, the term method is not unique to class instances: other object types can have methods as well. For example, list objects have methods called append, insert, remove, sort, and so on. However, in the following discussion, we’ll use the term method exclusively to mean methods of class instance objects, unless explicitly stated otherwise.)
Valid method names of an instance object depend on its class. By definition, all attributes of a class that are function objects define corresponding methods of its instances. So in our example, x.f is a valid method reference, since MyClass.f is a function, but x.i is not, since MyClass.i is not. But x.f is not the same thing as MyClass.f — it is a method object, not a function object.
9.3.4. Method Objects¶
Usually, a method is called right after it is bound:
In the MyClass example, this will return the string ‘hello world’ . However, it is not necessary to call a method right away: x.f is a method object, and can be stored away and called at a later time. For example:
will continue to print hello world until the end of time.
What exactly happens when a method is called? You may have noticed that x.f() was called without an argument above, even though the function definition for f() specified an argument. What happened to the argument? Surely Python raises an exception when a function that requires an argument is called without any — even if the argument isn’t actually used…
Actually, you may have guessed the answer: the special thing about methods is that the instance object is passed as the first argument of the function. In our example, the call x.f() is exactly equivalent to MyClass.f(x) . In general, calling a method with a list of n arguments is equivalent to calling the corresponding function with an argument list that is created by inserting the method’s instance object before the first argument.
If you still don’t understand how methods work, a look at the implementation can perhaps clarify matters. When a non-data attribute of an instance is referenced, the instance’s class is searched. If the name denotes a valid class attribute that is a function object, a method object is created by packing (pointers to) the instance object and the function object just found together in an abstract object: this is the method object. When the method object is called with an argument list, a new argument list is constructed from the instance object and the argument list, and the function object is called with this new argument list.
9.3.5. Class and Instance Variables¶
Generally speaking, instance variables are for data unique to each instance and class variables are for attributes and methods shared by all instances of the class:
As discussed in A Word About Names and Objects , shared data can have possibly surprising effects with involving mutable objects such as lists and dictionaries. For example, the tricks list in the following code should not be used as a class variable because just a single list would be shared by all Dog instances:
Разбираемся с доступом к атрибутам в Python
Интересно, сколько людей понимают, что в Python много синтаксического сахара? Я не говорю, что он похож на Lisp-подобные языки, где синтаксис настолько голый, насколько это возможно (хотя и сравнение с Lisp не совсем обосновано), но большая часть синтаксиса Python технически не нужна, поскольку под капотом в основном вызовы функций.
Ну и что с того? Зачем думать о том, как Python за меньший синтаксис делает больше вызовов функций? На самом деле для этого есть две причины. Во-первых, полезно знать, как на самом деле работает Python, чтобы лучше понимать/отлаживать код, когда что-то идет не так как надо. Во-вторых, так можно выявить минимум, необходимый для реализации языка.
Именно поэтому, чтобы заняться самообразованием и заодно подумать, что может понадобиться для реализации Python под WebAssembly или API bare bones на C, я решил написать эту статью о том, как выглядит доступ к атрибутам и что скрывается за синтаксисом.
Теперь вы можете попытаться собрать воедино все, что относится к доступу к атрибутам, прочитав справочник по Python. Так вы можете прийти к выражениям ссылок на атрибуты и модели данных для настройки доступа к атрибутам, однако, все равно важно связать все вместе, чтобы понять, как работает доступ. Поэтому я предпочитаю идти от исходного кода на CPython и выяснять, что происходит в интерпретаторе (я специально использую тег репозитория CPython 3.8.3, поскольку у меня есть стабильные ссылки и я использую последнюю версию на момент написания статьи).
В начале статьи вам встретится немного кода на С, но я не жду, что вы досконально поймете, что там происходит. Я напишу о том, что нужно будет из него понять, поэтому если у вас нет ни малейших знаний в С, то ничего страшного, вы все равно поймете все то, о чем я говорю.
Смотрим в байткод
Итак, давайте разберемся со следующим выражением:
Наверное, самое простая отправная точка в изучении – это байткод. Посмотрим на эту строку и разберемся, что делает компилятор:
Самый важный код операции здесь — LOADATTR. Если интересно, он заменяет объект на вершине стека результатом доступа к именованному атрибуту, как указано в conames[i] .
Цикл интерпретатора CPython лежит в Python/ceval.c. В его основе лежит массивный оператор switch , который ветвится в зависимости от выполняемого кода операции. Если заглянуть поглубже, то вы найдете следующие строки на С для LOADATTR :
Большая часть этого кода – это просто работа со стеком, его мы можем опустить. Ключевой бит – это вызов PyObject_GetAttr(), который и обеспечивает доступ к атрибутам.
Имя этой функции выглядит знакомо
Теперь это имя выглядит прямо как getattr(), только в соглашении об именовании функций в С, которое используется в CPython. Покопавшись в Python/bltinmodule.c, где лежат все встроенные модули Python, можем проверить, верна ли наша догадка. Поискав по «getattr» в файле, вы найдете строку, которая связывает имя «getattr» с функцией «builtin_getattr()»
Есть куча вещей, которые относятся к параметрам, но не интересуют нас, однако вы точно заметите, что когда вы передаете два аргумента в getattr() , будет вызван PyObjectGetAttr().
Что это означает? Ну, это означает, что вы можете «распаковать» obj.attr как getattr(obj, «attr») . А еще это означает, что если мы поймем как работает PyObjectGetAttr() , то поймем, как работает эта функция и, следовательно, как организован доступ к атрибутам в Python.
Разбираемся с getattr()
На этом я прекращу вставлять код на С, поскольку его сложность только растет, и он уже не настолько хорошо демонстрирует, что obj.attr это вариант написания getattr(obj, «attr»). Однако в комментариях псевдокода я продолжу на него ссылаться для тех, кто решил глубоко окунуться в CPython. Также обратите внимание, что код на Python следует рассматривать как псевдокод, поскольку в коде, реализующем доступ к атрибутам, есть сам по себе доступ к ним, но на уровне С он не проходит через обычный механизм доступа к атрибутам. Так что пока вы встречаете символ «.» , который используется в псевдокоде как синтаксис, знайте, что на уровне С доступ к атрибутам не рекурсивный и фактически функционирует так, как вы наивно можете предположить самостоятельно.
Что мы уже знаем
На данный момент о getattr() мы знаем три вещи. Во-первых, эта функция требует, как минимум, два атрибута. Во-вторых, второй аргумент должен быть подклассом str , в противном случае выпадет TypeError со статическим строковым аргументом (который, вероятно, статический из соображений производительности).
Запись функции для getattr()
Поиск атрибутов с помощью специальных методов
Доступ к атрибутам объекта осуществляется с помощью двух специальных методов. Первый метод – это getattribute(), который вызывается при попытке получить доступ ко всем атрибутам. Второй – это getattr(), который вызывает AttributeError . Первый метод (на сегодняшний день) всегда должен быть определен, тогда как второй метод является необязательным.
Python ищет специальные методы для типа объекта, а не для самого объекта. Чтобы внести ясность, скажу, что я очень специфически использую здесь слово «тип»: тип экземпляра – это его класс, тип класса – это его тип. К счастью, очень легко получить тип чего-либо благодаря конструктору type , возвращающему тип объекта: type(obj) .
Также нам нужно знать порядок разрешения метода (method resolution order, MRO). Так мы определим порядок иерархии типов для объекта. Алгоритм, который используется в Python пришел из языка Dylan и называется C3. Из кода на Python MRO раскрывается с помощью type(obj).mro() .
Обработка типа объекта осуществляется специально, поскольку это позволяет ускорить поиск и доступ. В целом, это исключает дополнительный поиск, пропуская экземпляр каждый раз, когда мы что-то ищем. На уровне CPython это позволяет заводить специальные методы, которые находятся в поле struct для ускорения поиска. Поэтому несмотря на то, что кажется немного странным игнорировать объект, а вместо него использовать тип, это имеет определенный смысл.
Теперь во имя простоты я немного схитрю и заставлю getattr() обрабатывать методы getattribute() и getattr() явно, в то время как CPython производит некоторые манипуляции под капотом, чтобы заставить объект обрабатывать оба метода самостоятельно. В конечном счете, семантика наших целей получается одинаковой.
Псевдокод, реализующий getattr()
Разбираемся с object.getattribute()
Несмотря на то, что мы можем получить реализацию getattr() , она, к сожалению, не расскажет нам много о работе Python и поиске атрибутов, поскольку очень большая часть обрабатывается в методе getattribute() объекта. Поэтому я расскажу как работает object.getattribute() .
В поисках дескриптора данных
Первая важная вещь, которую мы собираемся сделать в object.getattribute() – это поиск дескриптора данных для типа. Если вы никогда не слышали о дескрипторах, то расскажу – это способ программно управлять тем, как работает отдельный атрибут. Возможно, вы вообще никогда о них не слышали, но, если вы некоторое время уже используете Python, я подозреваю, что вы уже использовали дескрипторы: свойства, classmethod и staticmethod – все это дескрипторы.
Есть два типа дескрипторов: дескрипторы данных и дескрипторы без данных (non-data). Оба типа дескрипторов определяют метод get для получения того, каким должен быть атрибут. Дескрипторы данных также определяют методы set и del , в то время как дескрипторы без данных этого не делают. Свойство – это дескриптор данных, classmethod и staticmethod — это дескрипторы без данных.
Если мы не можем найти дескриптор данных для атрибута в типе, следующее место, где мы будем искать – это сам объект. Все оказывается просто благодаря объектам, имеющим атрибут dict , который хранит атрибуты самого объекта в словаре.
Если же у самого объекта нет атрибута, то мы увидим, есть ли там дескриптор без данных. Поскольку мы уже искали дескриптор ранее, то можем предположить, что если он был найден, но еще не использовался, когда мы искали дескриптор данных, то это дескриптор без данных.
Наконец, мы нашли атрибут типа, и он не был дескриптором, теперь мы возвращаем его. В итоге, порядок поиска атрибутов выглядит следующим образом:
Дескриптор данных ищется по типам;
Поиск по объекту;
Дескриптор без данных ищется по типам;
Что угодно ищется по типам.
Вы заметите, что сначала мы ищем какой-то дескриптор, затем, если нам это не удалось, мы ищем обычный объект, который соответствует виду дескриптора, который мы искали. Сначала мы ищем данные, потом уже что-то другое. Все это имеет смысл, если думать о том как метод self.attr = val в init() хранит данные об объекте. Скорее всего, если вы столкнулись с этим, то хотите, чтобы это стояло перед методом или чем-то подобным. И вам в первую очередь нужны дескрипторы, поскольку, если вы программно определили атрибут, то вероятно, хотели бы, чтобы он использовался всегда.
Заключение
Как видите, во время поиска атрибутов в Python происходит много интересного. Несмотря на то, что я бы сказал, что ни одна из частей не является концептуально сложной, в сумме мы получаем множество операций. Именно поэтому некоторые программисты пытаются минимизировать доступ к атрибутам в Python, чтобы избегать всего этого механизма, если речь идет о важности производительности.
Так исторически сложилось, что почти вся эта семантика пришла в Python как часть классов нового стиля, а не «классических». Это различие исчезло в Python 3, когда классические классы остались в прошлом, так что если вы ничего о них не слышали, то это и хорошо, наверное.
Другие статьи из этой серии можно найти по тегу «syntactic sugar» в этом блоге. Код из этой статьи вы найдете здесь.
Class and instance attributes in python
You may be a beginner coder trying to get into the amazing world of Object Oriented Programming, or maybe you are already an expert with just a little amnesia. In any case, it is always good to review concepts, and have a clear idea of what it is that you are coding and how you can use it to its full potential, in this case attributes.
Class and Objects
Let’s remember that a class is a sort of blueprint or template for creating code, more specifically objects. The latter being a contained component with methods and properties to make a particular set of data useful.
An example of this can be seen next:
In this case, an empty class called Employee is created. After this, we generate a new instance of that class, an object called emp1.
What’s a class attribute
When we create our class, we are also setting an enclosed space where our objects will exist. In this space we can have variables accessible by all our instances in a sort of global manner. These variables don’t belong to any object but the class itself.
In the next example, we create a class attribute called company_name. And up ahead we create two instances of the class Employee.
If we try to print the company_name with each instance, you can notice that it is the same for both.
What’s an instance attribute
If a class attribute is a variable that belongs to a class, and is available to all instances, then an instance attribute is a variable that belongs to a specific instance, and is only available to that instance.
In the next example we have two instances of the class Employee.
For the first instance, emp1, we assign a new attribute called name with the value “John”.
If we print emp1.name, we get the corresponding output. But if we try to print the same attribute, now with our second instance emp2, we will get an error.
This happens because name is an instance attribute, and belongs only to the object emp1.
What are all the ways to create them and what is the Pythonic way of doing it
You can create attributes for a class and for an instance in different ways.
Create class attribute outside the class declaration:
Create instance attributes one by one:
These ways of creating attributes are frowned upon though, as they require the programmer to repeat code and have little organization.
For class variables it is often preferred to have the declarations at the beginning of the class, before any methods.
For instance attributes it is recommended to use the __init__ method of the class, so that every time an instance is created, its attributes are automatically assigned.
What are the differences between class and instance attributes
Between this two kind of attributes, one should consider the following.
- If you want a single instance of the attribute (a single value) shared between all instances of the class, use a class attribute.
- If you want each instance of the class to have a distinct value of the attribute, use an instance attribute.
What are the advantages and drawbacks of each of them
Advantages of class attributes:
- They work like “global” variables for all the instances of your class
- They allow for operations and data management throughout all your objects belonging to the class.
Disadvantages of class attributes:
- Class attributes can become messy when instance attributes with the same names are created, meaning that its behavior can become unexpected.
Advantages of instance attributes:
- They are specific to an object
- They can coexist with class variables while having the same name
Disadvantages of instance attributes:
- They cannot be directly accessed by another instance if needed.
- Once the instance is deleted, its attributes are gone.
How does Python deal with the object and class attributes using the __dict__
Every object in Python has an attribute denoted by __dict__ .
This attribute contains a dictionary in the form key: value, where all of the other attributes are stored.
If for example we create the Class Employee with the attribute company_name and the we print its __dict__, we will see several variables that belong to the class, including the attribute we just declared.
If we now create a new instance of the class and try to print its dictionary, we will see an empty data structure. Still, if we try to print its attribute company_name, we won’t get any errors.
This is due to the fact that python first searches for the attribute in the emp1 dictionary. If the attribute is not found there, python proceeds to look for it in the class space, where it is then printed.
It is possible to assign a new attribute to the emp1 instance having the same name as the class attribute.
If the dictionary of emp1 is checked again, the attribute just created can be found and printed.
This does not mean the class attribute is replaced, as it still exists, and can be accessed.
Доступ к атрибутам класса
Доступ к базовым атрибутам с помощью точечной нотации
Давайте возьмем образец класса.
В Python вы можете получить доступ названия атрибута класса с помощью точечной нотации.
Если атрибут не существует, Python выдает ошибку:
Сеттеры, геттеры и свойства
Для инкапсуляции данных иногда требуется иметь атрибут, значение которого исходит из других атрибутов или, вообще, какое значение должно быть вычислено в данный момент. Стандартный способ справиться с этой ситуацией — создать метод, называемый getter или setter.
В приведенном выше примере легко увидеть, что произойдет, если мы создадим новую книгу, которая содержит заголовок и автора. Если все книги, которые мы добавляем в нашу Библиотеку, имеют авторов и заголовки, то мы можем пропустить методы получения и установки и использовать точечную запись. Однако предположим, что у нас есть книги, у которых нет автора, и мы хотим установить для автора значение «Неизвестно». Или, если у них несколько авторов, и мы планируем вернуть список авторов.
В этом случае мы можем создать геттер и сеттер для атрибута автора.
Эта схема не рекомендуется.
Одна из причин в том, что здесь есть одна загвоздка: давайте предположим, что мы разработали наш класс с открытым атрибутом и без методов. Люди уже много его использовали, и они написали такой код:
Теперь у нас есть проблема. Поскольку автор не является атрибутом! Python предлагает решение этой проблемы, называемое свойствами. Метод для получения свойств украшен @property перед его заголовком. Метод, который мы хотим функционировать как установщик, перед ним имеет атрибут @ attributeName.setter.
Учитывая это, у нас теперь есть новый обновленный класс.
Обратите внимание, что обычно Python не позволяет использовать несколько методов с одинаковым именем и разным количеством параметров. Однако в этом случае Python позволяет это из-за используемых декораторов.