UuidInterface.ToString() raise a TypeError.
-
Hi there,
I had test of maxon.UuidInterface and the ToString() seems not work.
return:
14014181541439318986 Traceback (most recent call last): File "scriptmanager", line 5, in <module> File "C:\Program Files\Maxon Cinema 4D 2024\resource\modules\python\libs\python311\maxon\decorators.py", line 553, in NativeDataOrBuiltin return ExecMethod(self._data, *args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: net.maxon.interface.uuid.ToString@a73a45c584c879d4() takes exactly 1 argument (0 given)
code:
import maxon uid = maxon.UuidInterface.Alloc() print(uid.GetHashCode()) print(uid.ToString())
-
Hey @Dunhou,
Thank you for reaching out to us. You have stumbled here upon something I have stumbled myself more than once, the weird errors when you try to instantiate the "wrong class" in Python. There are less obvious cases, you for example must instantiate a
maxon.Vector
in Python and cannot instantiate amaxon.Vec3
(which actually holds all the documentation) - it can be quite confusing but has been declared as intentional by the developers.TLDR: You did use the incorrect type to instantiate the Uuid, you must use the reference, not the interface. So there is no bug here, the error messages are just a bit cryptic.
Atomic Type and Memory Management
When you dip your toes into the maxon API, you have to understand a central concept, interfaces and references. Types postfixed with the word
Interface
, e.g., maxon.UuidInterface, maxon.FileAssetInterface, or maxon.GraphModelInterface, realize a type in the object system of Cinema 4D, i.e., derive from maxon.ObjectInterface.ObjectInterface
is quite similar to Python's ownobject
as it implements the atomic type for 'things' in the API (to get a better grasp of the type you should look at its C++ version as the Python one is mostly a sealed wrapper).The object system realizes functionalities one would expect from an object system, like comparing objects, hashing them, getting strings representing them, or poking around in the type metadata of an instance, i.e., it is quite similar to what Python's
object
offers. And just like Python'sobject
,ObjectInterface
also comes with the concept of memory management.Many modern programming languages use the concept of automated memory management. Because in so-called unmanaged languages, e.g., C++, you have to handle your memory manually when you want something to live longer than the function scope it is allocated in. But this manual memory management is very error prone, as it produces the infamous memory leaks when we forget to deallocate the memory we have allocated.
Automated memory management as realized by languages like Python, Java, or C# tries to solve this problem by not asking the user to handle memory deallocation anymore and instead automating it. There are several strategies for this - which are also not mutually exclusive, as Python employs both ideas of garbage collection and reference counting for example - but the for us relevant is here reference counting. Cinema 4D also realizes reference counting with its interface and reference system.
Objects and References
Reference counting implies a system of objects and references, i.e., the actual data and references to it. So, when we have this Python code we can understand the concept of objects and references:
class User: # Could also write `class User (object)` when we want to be more verbose. def __init__(self, name: str, friend: "User" | None = None) -> None: self.Name: str = str(name) self.Friend: "User" | None = friend # Both statements (user_1, user_2) do two things: 1. They (heap) allocate a `User` object and put # it into the Python object table. 2. They draw a reference to that object and assign it to the # "variable" the user created (user_1, user_2). So, these variables do not actually hold the object # but only a reference to it. The second statement (user_2) then also passes the reference to "Alice" # to its `User` constructor, and with that draws a new reference. user_1: User = User("Alice") user_2: User = User("Bob", user_1) # Reference count at this point: # "Alice": 2 (user_1, user_2.Friend) # "Bob": 1 (user_2) # This means even when the reference user_1 is being deallocated, the "Alice" object will not be # deleted because "Bob".Friend still holds a reference.
So, the variables we assign the
User
objects to do not actually hold the object but only a reference to it. Which also means that when we have two references of one object we do not need twice the memory but only must pay the price for another reference. We can look at the same example in C#:public class User { public string Name; public User Friend; public User(string name, User friend = null) { Name = name; Friend = friend; } } User user1 = new User("Alice"); User user2 = new User("Bob", user1);
Conceptually things are very similar here. We again create objects and draw references to them, the
new
keyword in C# makes it more clear where a new object is allocated and where not. Other than Python and the maxon API, C# is not using reference counting for its memory management but a scope based garbage collector. But the concept of objects and references is independent of the concept of reference counting.Interfaces and References
The maxon API operates by the very same principles of objects and references, and implements its garbage collection via reference counting (there are some differences to Python's version of reference counting which I will ignore here). The major difference to Python, C#, Java, etc. is that the maxon API exposes types for both the reference (which we assign to variables) and the actual object (which would live in magic object table land in Python). An interface, e.g.,
maxon.GraphModelInterface
realizes the actual object which is then accompanied by a reference counterpart, in this casemaxon.GraphModelRef
. So, in code you usually deal with the references and not with the actual object. The reference can do everything the object/interface can, i.e., has access to all its functions.And because that all its not yet confusing enough term-wise
, the maxon AP then also makes a distinction between COW (Copy-on-Write) and non-COW interfaces in its naming conventions. To understand this, we will have a look at our user example again:
# We allocate a User "Alice" and draw two references for it. user_alice: User = User("Alice") user_current: User = user_alice # ... # We now hit a condition in our code where we want to rename our current user to "Bob". if condition: user_current.Name = "Bob" # But since we operate here on one object, this will also change the name of the reference user_alice. print (user_alice.Name) # Bob
References have the risk or advantage (depending on the point of view) that we deal with shared data. In some cases, for example our
User
model, we could declare this a feature, in other cases not so much. Strings for example are also references in Python (and many other languages). When we have ten variables with the string"Hello World!"
, then there will be exactly one string object for this in the Python object table. When we now edit the string of one of the variables, it would be rather odd and not useful, if the string of the other variables would also change.Python labels this as immutable types in its type system, which then leads to confusing statements like that
str
andint
(in the interval[-5, 256]
) are immutable. What Python's means with that, is that it will not try to write to the referenced object for these types when a write operation occurs but instead create a new object (so that the data for other reference holders of the original object does not change).a: str = "foo" b: str = "foo" print(id(a) == id(b)) # Prints "True", as a and b reference the same object. # When we now try to write to the object referenced by #b, something new happens, we do not also # change the value for all other reference holders. That is because strings are in Python "immutable" # or in other words, copy-on-write, i.e., we can never edit an existing string object but rather must # draw a new copy. Python's immutability system is quite complex and I am ignoring some details here. b = "bar" print (a, b) # Prints "foo bar" print(id(a) == id(b)) # Prints "False", a and b are not the same object anymore.
COW (Copy-on-Write) is pretty much the same thing as Python's immutability system. In the C++ maxon API it is for once expressed when an interface is declared with
MAXON_REFERENCE_COPY_ON_WRITE
(Link). Another marker is the a bit confusing naming scheme of references.- Objects that will not be copied when the user tries to edit data end in
Ref
, e.g.,GraphModelInterface
andGraphModelRef
. - Objects that will be copied when the user tries to edit them (COW), do not end in
Ref
, e.g.,UrlInterface
andUrl
orUuidInterface
andUuid
. Do not ask me why we did it like that, I find it confusing too.
Other than in Python, where the selection of immutable types in that sense is quite limited, there are quite a few in the maxon API. Many (but not all) things that derive from
maxon.Data
are COW: Urls, uuids, strings, colors, vectors, gradients and many more. So, that an asset has for example the typemaxon.Asset
and notmaxon.AssetRef
indeed means that assets are copy-on-write.Using References and Interfaces in the maxon API
Using the system is then fairly straight forward. To allocate a new instance, one must use one of the
Create
methods found on the reference. TheAllocate
methods found on the object/interface are backend methods for allocating the actual object which users most of the time do not have to use.import maxon language: maxon.LanguageRef = maxon.LanguageRef.Create()
Creating references for non-COW, i.e., mutable, objects in this manner is however not too meaningful in Python at the moment, as you are usually passed the references by some helper function (find a node in a graph, get the active asset database etc.). More meaningful is the COW case. It is the same here, but here we have actually
Create
functions which allow us to pass data.import maxon import uuid # Create a Uuid instance without any arguments. a: maxon.Uuid = maxon.Uuid.Create() print(f"{a = }") # > a = maxon.Uuid object at 0x7fd11ae1bfa0 with the data: 667FBB24-4ADE-451B-823B-6DC5996F04FB # Create an Uuid using Python's uuid module and pass its string to the Create constructor. myUuid: str = str(uuid.uuid4()) b: maxon.Uuid = maxon.Uuid.Create(myUuid) print(f"{b = }, {myUuid = }") # > b = maxon.Uuid object at 0x7fd11ae21760 with the data: CB81AF5D-4535-42C8-9A91-7F821441CB28, # myUuid = 'cb81af5d-4535-42c8-9a91-7f821441cb28'
Last but not least, there are usually also wrappers for
Create
to use the more Pythonic__init__
instead. E.g.:import maxon import uuid a: maxon.Uuid = maxon.Uuid() b: maxon.Uuid = maxon.Uuid(str(uuid.uuid4())) print (a) print (b) # > E8C2DD7D-8768-4C52-8F39-193574C1761E # > D4480A8E-CB0C-4B42-B1F9-AE38B6B8BA86
Conclusion
- The maxon API realizes an object-reference system for memory management.
- As a user, the reference is usually the side we work with, it is equal to what we perceive in Python or C# as "the object".
- Mutable object references are postfixed with "Ref" while immutable objects, i.e., COW-references do not have any postfix and instead just drop the "Interface" from the object name.
- Objects are effectively allocated with the
Create
functions found on refereences. They do the same as what object allocation does in Python, Java, C#, etc. does, they allocate the object and return a reference to it (and hide the actual object instance away from us). TheAllocate
functions found on interfaces are only for hyper-experts useful.
Tip: When I write Python code in the maxon API for myself, I tend to deliberately type-hint objects incorrectly. I.e., I type hint
foo: maxon.FooInterface = data.Get()
even thoughGet
technically returns aFoo
orFooRef
. The advantage is that we will now see autocompletions for the interface (where all the important methods and fields are defined) and not for the reference (where almost nothing is defined). References provide access to all members of their interface, but since interface and references are not in an inheritance relationship, the autocomplete does not see that.Cheers,
Ferdinand - Objects that will not be copied when the user tries to edit data end in
-
Wow @ferdinand๏ผ
What a super detailed and super useful information ! Always learning great things from your super user-friendly answers for rookies like me
Memory Management is very useful to understand what happened here, I will read it again tomorrow after work, hope I can have a deeper understanding then
About the Interfaces/References/Ref class name, super useful information, I always confused them and thought it was an inheritance relationship or something like that, it is much cleaner now!
Thanks for your typing tips here, I had learn this by testing your answers and codes (In fact, most of my programming skills are learned through studying your code, and I am truly grateful to you)
Last๏ผ thanks or your times during the meeting time!
Cheers~
DunHou